comment issue #702 - Add Bubble timeline

This commit is contained in:
Thomas 2023-01-01 11:54:43 +01:00
parent f976b6dd72
commit 8fb7e4dc71
12 changed files with 420 additions and 3 deletions

View file

@ -683,6 +683,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
regex_local = sharedpreferences.getString(getString(R.string.SET_FILTER_REGEX_LOCAL) + currentUserID + currentInstance, null);
regex_public = sharedpreferences.getString(getString(R.string.SET_FILTER_REGEX_PUBLIC) + currentUserID + currentInstance, null);
show_art_nsfw = sharedpreferences.getBoolean(getString(R.string.SET_ART_WITH_NSFW) + currentUserID + currentInstance, false);
binding.profilePicture.setOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START));
Helper.loadPP(BaseMainActivity.this, binding.profilePicture, currentAccount);
headerMainBinding.accountAcc.setText(String.format("%s@%s", currentAccount.mastodon_account.username, currentAccount.instance));

View file

@ -0,0 +1,35 @@
package app.fedilab.android.client.entities.app;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
public class BubbleTimeline implements Serializable {
@SerializedName("id")
public int id;
@SerializedName("only_media")
public boolean only_media = false;
@SerializedName("remote")
public boolean remote = false;
@SerializedName("with_muted")
public boolean with_muted;
@SerializedName("exclude_visibilities")
public List<String> exclude_visibilities = null;
@SerializedName("reply_visibility")
public String reply_visibility = null;
}

View file

@ -38,6 +38,8 @@ public class PinnedTimeline implements Serializable {
public RemoteInstance remoteInstance;
@SerializedName("tagTimeline")
public TagTimeline tagTimeline;
@SerializedName("bubbleTimeline")
public BubbleTimeline bubbleTimeline;
@SerializedName("mastodonList")
public MastodonList mastodonList;
@SerializedName("currentFilter")

View file

@ -257,6 +257,7 @@ public class Helper {
public static final String ARG_SEARCH_KEYWORD_CACHE = "ARG_SEARCH_KEYWORD_CACHE";
public static final String ARG_VIEW_MODEL_KEY = "ARG_VIEW_MODEL_KEY";
public static final String ARG_TAG_TIMELINE = "ARG_TAG_TIMELINE";
public static final String ARG_BUBBLE_TIMELINE = "ARG_BUBBLE_TIMELINE";
public static final String ARG_MEDIA_POSITION = "ARG_MEDIA_POSITION";
public static final String ARG_MEDIA_ATTACHMENT = "ARG_MEDIA_ATTACHMENT";
public static final String ARG_MEDIA_ATTACHMENTS = "ARG_MEDIA_ATTACHMENTS";

View file

@ -14,6 +14,7 @@ package app.fedilab.android.helper;
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.currentAccount;
import static app.fedilab.android.BaseMainActivity.currentInstance;
import static app.fedilab.android.BaseMainActivity.currentUserID;
@ -59,6 +60,7 @@ import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.api.MastodonList;
import app.fedilab.android.client.entities.app.BottomMenu;
import app.fedilab.android.client.entities.app.BubbleTimeline;
import app.fedilab.android.client.entities.app.Pinned;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.client.entities.app.RemoteInstance;
@ -66,6 +68,8 @@ import app.fedilab.android.client.entities.app.StatusCache;
import app.fedilab.android.client.entities.app.TagTimeline;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.ActivityMainBinding;
import app.fedilab.android.databinding.DialogBubbleExcludeVisibilityBinding;
import app.fedilab.android.databinding.DialogBubbleReplyVisibilityBinding;
import app.fedilab.android.databinding.TabCustomDefaultViewBinding;
import app.fedilab.android.databinding.TabCustomViewBinding;
import app.fedilab.android.exception.DBException;
@ -498,6 +502,9 @@ public class PinnedTimelineHelper {
case TAG:
tagClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
break;
case BUBBLE:
bubbleClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
break;
case REMOTE:
if (pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
instanceClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
@ -980,6 +987,244 @@ public class PinnedTimelineHelper {
}
/**
* Manage long clicks on Bubble timelines
*
* @param activity - BaseMainActivity activity
* @param pinned - {@link Pinned}
* @param view - View
* @param position - int position of the tab
*/
public static void bubbleClick(BaseMainActivity activity, Pinned pinned, View view, ActivityMainBinding activityMainBinding, int position, String slug) {
int toRemove = itemToRemoveInBottomMenu(activity);
PopupMenu popup = new PopupMenu(activity, view);
int offSetPosition = position - (BOTTOM_TIMELINE_COUNT - toRemove);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity);
boolean singleBar = sharedpreferences.getBoolean(activity.getString(R.string.SET_USE_SINGLE_TOPBAR), false);
if (singleBar) {
offSetPosition = position;
}
if (pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline == null) {
pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline = new BubbleTimeline();
}
BubbleTimeline bubbleTimeline = pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline;
popup.getMenuInflater()
.inflate(R.menu.option_bubble_timeline, popup.getMenu());
Menu menu = popup.getMenu();
final MenuItem itemMediaOnly = menu.findItem(R.id.action_show_media_only);
final MenuItem itemRemote = menu.findItem(R.id.action_remote);
final boolean[] changes = {false};
final boolean[] mediaOnly = {false};
final boolean[] remote = {false};
mediaOnly[0] = bubbleTimeline.only_media;
remote[0] = bubbleTimeline.remote;
itemMediaOnly.setChecked(mediaOnly[0]);
itemRemote.setChecked(remote[0]);
popup.setOnDismissListener(menu1 -> {
if (changes[0]) {
if (activityMainBinding.viewPager.getAdapter() != null) {
try {
new StatusCache(activity).deleteForSlug(slug);
} catch (DBException e) {
e.printStackTrace();
}
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putString(activity.getString(R.string.SET_INNER_MARKER) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance + slug, null);
editor.commit();
Fragment fragmentMastodonTimeline = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
if (fragmentMastodonTimeline instanceof FragmentMastodonTimeline && fragmentMastodonTimeline.isVisible()) {
FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction();
fragTransaction.detach(fragmentMastodonTimeline).commit();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.BUBBLE);
bundle.putSerializable(Helper.ARG_BUBBLE_TIMELINE, bubbleTimeline);
bundle.putSerializable(Helper.ARG_INITIALIZE_VIEW, false);
fragmentMastodonTimeline.setArguments(bundle);
FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction();
fragTransaction2.attach(fragmentMastodonTimeline);
fragTransaction2.commit();
((FragmentMastodonTimeline) fragmentMastodonTimeline).recreate();
}
}
}
});
int finalOffSetPosition = offSetPosition;
popup.setOnMenuItemClickListener(item -> {
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
item.setActionView(new View(activity));
item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return false;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
return false;
}
});
changes[0] = true;
int itemId = item.getItemId();
if (itemId == R.id.action_show_media_only) {
mediaOnly[0] = !mediaOnly[0];
bubbleTimeline.only_media = mediaOnly[0];
pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline;
itemMediaOnly.setChecked(mediaOnly[0]);
try {
new Pinned(activity).updatePinned(pinned);
} catch (DBException e) {
e.printStackTrace();
}
} else if (itemId == R.id.action_remote) {
remote[0] = !remote[0];
bubbleTimeline.remote = remote[0];
pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline;
itemRemote.setChecked(remote[0]);
try {
new Pinned(activity).updatePinned(pinned);
} catch (DBException e) {
e.printStackTrace();
}
} else if (itemId == R.id.action_exclude_visibility) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle());
DialogBubbleExcludeVisibilityBinding dialogBinding = DialogBubbleExcludeVisibilityBinding.inflate(activity.getLayoutInflater());
dialogBuilder.setView(dialogBinding.getRoot());
dialogBuilder.setTitle(R.string.exclude_visibility);
if (bubbleTimeline.exclude_visibilities == null) {
bubbleTimeline.exclude_visibilities = new ArrayList<>();
}
for (String value : bubbleTimeline.exclude_visibilities) {
if (value.equalsIgnoreCase("public")) {
dialogBinding.valuePublic.setChecked(true);
}
if (value.equalsIgnoreCase("local")) {
dialogBinding.valueLocal.setChecked(true);
}
if (value.equalsIgnoreCase("direct")) {
dialogBinding.valueDirect.setChecked(true);
}
if (value.equalsIgnoreCase("list")) {
dialogBinding.valueList.setChecked(true);
}
if (value.equalsIgnoreCase("private")) {
dialogBinding.valuePrivate.setChecked(true);
}
if (value.equalsIgnoreCase("unlisted")) {
dialogBinding.valueUnlisted.setChecked(true);
}
}
dialogBinding.valuePrivate.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("private")) {
bubbleTimeline.exclude_visibilities.add("private");
}
} else {
bubbleTimeline.exclude_visibilities.remove("private");
}
});
dialogBinding.valueDirect.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("direct")) {
bubbleTimeline.exclude_visibilities.add("direct");
}
} else {
bubbleTimeline.exclude_visibilities.remove("direct");
}
});
dialogBinding.valueList.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("list")) {
bubbleTimeline.exclude_visibilities.add("list");
}
} else {
bubbleTimeline.exclude_visibilities.remove("list");
}
});
dialogBinding.valueLocal.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("local")) {
bubbleTimeline.exclude_visibilities.add("local");
}
} else {
bubbleTimeline.exclude_visibilities.remove("local");
}
});
dialogBinding.valuePublic.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("public")) {
bubbleTimeline.exclude_visibilities.add("public");
}
} else {
bubbleTimeline.exclude_visibilities.remove("public");
}
});
dialogBinding.valueUnlisted.setOnCheckedChangeListener((compoundButton, checked) -> {
if (checked) {
if (!bubbleTimeline.exclude_visibilities.contains("unlisted")) {
bubbleTimeline.exclude_visibilities.add("unlisted");
}
} else {
bubbleTimeline.exclude_visibilities.remove("unlisted");
}
});
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline;
try {
new Pinned(activity).updatePinned(pinned);
} catch (DBException e) {
e.printStackTrace();
}
});
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();
} else if (itemId == R.id.action_reply_visibility) {
AlertDialog.Builder dialogBuilder;
AlertDialog alertDialog;
dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle());
DialogBubbleReplyVisibilityBinding dialogBinding = DialogBubbleReplyVisibilityBinding.inflate(activity.getLayoutInflater());
dialogBuilder.setView(dialogBinding.getRoot());
dialogBuilder.setTitle(R.string.reply_visibility);
int checkedId = R.id.all;
if (bubbleTimeline.reply_visibility != null && bubbleTimeline.reply_visibility.equalsIgnoreCase("following")) {
checkedId = R.id.following;
} else if (bubbleTimeline.reply_visibility != null && bubbleTimeline.reply_visibility.equalsIgnoreCase("self")) {
checkedId = R.id.self;
}
dialogBinding.replyVisibility.check(checkedId);
dialogBinding.replyVisibility.setOnCheckedChangeListener((radioGroup, checkedElement) -> {
if (checkedElement == R.id.all) {
bubbleTimeline.reply_visibility = null;
} else if (checkedElement == R.id.following) {
bubbleTimeline.reply_visibility = "following";
} else if (checkedElement == R.id.self) {
bubbleTimeline.reply_visibility = "self";
}
});
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline;
try {
new Pinned(activity).updatePinned(pinned);
} catch (DBException e) {
e.printStackTrace();
}
});
alertDialog = dialogBuilder.create();
alertDialog.show();
}
return false;
});
popup.show();
}
/**
* Manage long clicks on followed instances
*

View file

@ -51,6 +51,7 @@ import app.fedilab.android.client.entities.api.Attachment;
import app.fedilab.android.client.entities.api.Pagination;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.api.Statuses;
import app.fedilab.android.client.entities.app.BubbleTimeline;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.client.entities.app.RemoteInstance;
import app.fedilab.android.client.entities.app.StatusCache;
@ -165,6 +166,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
private Statuses initialStatuses;
private String list_id;
private TagTimeline tagTimeline;
private BubbleTimeline bubbleTimeline;
private LinearLayoutManager mLayoutManager;
private Account accountTimeline;
private boolean exclude_replies, exclude_reblogs, show_pinned, media_only, minified;
@ -331,6 +333,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
isViewInitialized = getArguments().getBoolean(Helper.ARG_INITIALIZE_VIEW, true);
isNotPinnedTimeline = isViewInitialized;
tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE);
bubbleTimeline = (BubbleTimeline) getArguments().getSerializable(Helper.ARG_BUBBLE_TIMELINE);
accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT);
exclude_replies = !getArguments().getBoolean(Helper.ARG_SHOW_REPLIES, true);
checkRemotely = getArguments().getBoolean(Helper.ARG_CHECK_REMOTELY, false);
@ -354,6 +357,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
if (tagTimeline.isART) {
timelineType = Timeline.TimeLineEnum.ART;
}
} else if (bubbleTimeline != null) {
ident = "@B@Bubble";
} else if (list_id != null) {
ident = "@l@" + list_id;
} else if (remoteInstance != null && !checkRemotely) {
@ -713,8 +718,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelineParams.remote = true;
break;
case BUBBLE:
timelineParams.onlyMedia = false;
timelineParams.remote = false;
timelineParams.onlyMedia = bubbleTimeline.only_media;
timelineParams.remote = bubbleTimeline.remote;
timelineParams.replyVisibility = bubbleTimeline.reply_visibility;
timelineParams.excludeVisibilities = bubbleTimeline.exclude_visibilities;
break;
case LIST:
timelineParams.listId = list_id;

View file

@ -137,6 +137,8 @@ public class FedilabPageAdapter extends FragmentStatePagerAdapter {
bundle.putSerializable(Helper.ARG_TAG_TIMELINE, pinnedTimeline.tagTimeline);
} else if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) {
bundle.putSerializable(Helper.ARG_REMOTE_INSTANCE, pinnedTimeline);
} else if (pinnedTimeline.type == Timeline.TimeLineEnum.BUBBLE) {
bundle.putSerializable(Helper.ARG_BUBBLE_TIMELINE, pinnedTimeline.bubbleTimeline);
}
}

View file

@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/fab_margin">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_public"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/v_public"
android:textSize="16sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_unlisted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/v_unlisted"
android:textSize="16sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_local"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/local"
android:textSize="16sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_private"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/v_private"
android:textSize="16sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_direct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/v_direct"
android:textSize="16sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/value_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@string/v_list"
android:textSize="16sp" />
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/fab_margin">
<RadioGroup
android:id="@+id/reply_visibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/all"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/following"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/following"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/self"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/self"
android:textSize="16sp" />
</RadioGroup>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_show_media_only"
android:checkable="true"
android:title="@string/show_media_only"
app:actionViewClass="android.widget.CheckBox"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/action_remote"
android:checkable="true"
android:title="@string/remote"
app:actionViewClass="android.widget.CheckBox"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/action_exclude_visibility"
android:title="@string/exclude_visibility"
app:showAsAction="always" />
<item
android:id="@+id/action_reply_visibility"
android:title="@string/reply_visibility"
app:showAsAction="always" />
</menu>

View file

@ -1365,6 +1365,8 @@
<string name="SET_LED_COLOUR_VAL_N" translatable="false">SET_LED_COLOUR_VAL_N</string>
<string name="SET_SHOW_BOOSTS" translatable="false">SET_SHOW_BOOSTS</string>
<string name="SET_SHOW_REPLIES" translatable="false">SET_SHOW_REPLIES</string>
<string name="SET_DISABLE_ANIMATED_EMOJI" translatable="false">SET_DISABLE_ANIMATED_EMOJI</string>
<string name="SET_CAPITALIZE" translatable="false">SET_CAPITALIZE</string>
<string name="SET_THEME_BASE" translatable="false">SET_THEME_BASE</string>
@ -2175,4 +2177,9 @@
<string name="set_display_quote_indication">Display the \"Quote\" button</string>
<string name="set_display_reaction_indication">Display \"Reactions\" buttons</string>
<string name="bubble">Bubble</string>
<string name="exclude_visibility">Exclude visibility</string>
<string name="reply_visibility">Reply visibility</string>
<string name="v_list">List</string>
<string name="following">Following</string>
<string name="self">Self</string>
</resources>