Merge remote-tracking branch 'megalodon_main/main' into m3-merger

# Conflicts:
#	README.md
#	build.gradle
#	mastodon/build.gradle
#	mastodon/src/main/AndroidManifest.xml
#	mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
#	mastodon/src/main/java/org/joinmastodon/android/MainActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetAccountMuted.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/PinnableStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileAboutFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/CustomWelcomeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/Attachment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/Status.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/CustomEmojiPopupKeyboard.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
#	mastodon/src/main/res/color/button_bg_secondary_dark_on_light.xml
#	mastodon/src/main/res/color/button_text_primary_light_on_dark.xml
#	mastodon/src/main/res/drawable/bg_image_alt_text_overlay.xml
#	mastodon/src/main/res/drawable/bg_rect_4dp_ripple.xml
#	mastodon/src/main/res/drawable/bg_search_field.xml
#	mastodon/src/main/res/drawable/ic_fluent_save_24_regular.xml
#	mastodon/src/main/res/layout/compose_action.xml
#	mastodon/src/main/res/layout/compose_media_thumb.xml
#	mastodon/src/main/res/layout/compose_poll_option.xml
#	mastodon/src/main/res/layout/display_item_footer.xml
#	mastodon/src/main/res/layout/display_item_header.xml
#	mastodon/src/main/res/layout/display_item_text.xml
#	mastodon/src/main/res/layout/fragment_compose.xml
#	mastodon/src/main/res/layout/fragment_profile.xml
#	mastodon/src/main/res/layout/item_instance_category.xml
#	mastodon/src/main/res/layout/item_report_choice.xml
#	mastodon/src/main/res/layout/item_settings_footer.xml
#	mastodon/src/main/res/layout/item_settings_switch.xml
#	mastodon/src/main/res/layout/item_settings_theme.xml
#	mastodon/src/main/res/layout/item_settings_theme_subitem.xml
#	mastodon/src/main/res/layout/item_settings_update.xml
#	mastodon/src/main/res/layout/tab_bar.xml
#	mastodon/src/main/res/menu/mute_duration.xml
#	mastodon/src/main/res/values-de-rDE/strings_sk.xml
#	mastodon/src/main/res/values-es-rES/strings_sk.xml
#	mastodon/src/main/res/values-fa/strings_sk.xml
#	mastodon/src/main/res/values-night/colors.xml
#	mastodon/src/main/res/values-nl-rNL/strings_sk.xml
#	mastodon/src/main/res/values-uk-rUA/strings_sk.xml
#	mastodon/src/main/res/values-zh-rCN/strings_sk.xml
#	mastodon/src/main/res/values/attrs.xml
#	mastodon/src/main/res/values/ids.xml
#	mastodon/src/main/res/values/styles.xml
#	metadata/es/changelogs/83.txt
This commit is contained in:
LucasGGamerM
2023-08-20 11:36:16 -03:00
parent 8f4363a2d8
commit 93818903c8
753 changed files with 28298 additions and 14085 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:tools="http://schemas.android.com/tools"
package="org.joinmastodon.android"> xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -30,7 +30,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Mastodon.AutoLightDark" android:theme="@style/Theme.Mastodon.AutoLightDark"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustPan"
android:largeHeap="true"> android:largeHeap="true">
@@ -49,7 +48,6 @@
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
<intent-filter> <intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" /> <action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
@@ -92,6 +90,15 @@
<category android:name="me.grishka.fcmtest"/> <category android:name="me.grishka.fcmtest"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushNotificationReceiver"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
</intent-filter>
</receiver>
<provider <provider
android:name="org.joinmastodon.android.utils.FileProvider" android:name="org.joinmastodon.android.utils.FileProvider"

View File

@@ -1,6 +1,7 @@
13bells.com 13bells.com
1611.social
4aem.com 4aem.com
aethy.com adachi.party
anime.website anime.website
annihilation.social annihilation.social
anon-kenkai.com anon-kenkai.com
@@ -9,14 +10,17 @@ bae.st
bajax.us bajax.us
banepo.st banepo.st
baraag.net baraag.net
bassam.social
beefyboys.win beefyboys.win
beepboop.ga beepboop.ga
berserker.town berserker.town
bikeshed.party bikeshed.party
boks.moe boks.moe
boymoder.biz
brainsoap.net brainsoap.net
breastmilk.club breastmilk.club
brighteon.social brighteon.social
bungle.online
cawfee.club cawfee.club
clew.lol clew.lol
clubcyberia.co clubcyberia.co
@@ -25,8 +29,8 @@ comfyboy.club
contrapointsfan.club contrapointsfan.club
cum.camp cum.camp
cum.salon cum.salon
cybercriminal.eu
darknight-coffee.org darknight-coffee.org
decayable.ink
dembased.xyz dembased.xyz
desupost.soy desupost.soy
detroitriotcity.com detroitriotcity.com
@@ -40,7 +44,6 @@ fluf.club
foxfam.club foxfam.club
freak.university freak.university
freeatlantis.com freeatlantis.com
freecumextremist.com
freedomstrike.org freedomstrike.org
freesoftwareextremist.com freesoftwareextremist.com
freespeech.group freespeech.group
@@ -59,7 +62,6 @@ goyim.app
goyslop.cafe goyslop.cafe
haeder.net haeder.net
handholding.io handholding.io
hidamari.apartments
hitchhiker.social hitchhiker.social
hunk.city hunk.city
iddqd.social iddqd.social
@@ -75,14 +77,13 @@ leftychan.net
lewdieheaven.com lewdieheaven.com
liberdon.com liberdon.com
ligma.pro ligma.pro
lizards.live
lolicon.rocks lolicon.rocks
lolison.top lolison.top
lovingexpressions.net lovingexpressions.net
lucasvl.nl
mahodou.moe mahodou.moe
makemysarcophagus.com makemysarcophagus.com
maladaptive.art maladaptive.art
marsey.moe
masochi.st masochi.st
mastinator.com mastinator.com
merovingian.club merovingian.club
@@ -105,7 +106,6 @@ nobodyhasthe.biz
nukem.biz nukem.biz
obo.sh obo.sh
onionfarms.org onionfarms.org
outpoa.st
pawlicker.com pawlicker.com
pawoo.net pawoo.net
pedo.school pedo.school
@@ -121,6 +121,7 @@ poast.tv
poster.place poster.place
prospeech.space prospeech.space
quodverum.com quodverum.com
r18.social
rakket.app rakket.app
rapemeat.solutions rapemeat.solutions
rdrama.cc rdrama.cc
@@ -132,7 +133,6 @@ schwartzwelt.xyz
seal.cafe seal.cafe
shigusegubu.club shigusegubu.club
shitpost.cloud shitpost.cloud
shitposter.club
shota.house shota.house
silliness.observer silliness.observer
skinheads.eu skinheads.eu
@@ -149,7 +149,6 @@ sonichu.com
spinster.xyz spinster.xyz
springbo.cc springbo.cc
starnix.network starnix.network
stereophonic.space
strelizia.net strelizia.net
syspxl.xyz syspxl.xyz
tastingtraffic.net tastingtraffic.net

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<style>
*{
box-sizing: border-box;
overflow-wrap: break-word;
}
body{
background: {{colorSurface}};
padding: 16px 16px 0 16px;
margin: 0;
color: {{colorOnSurface}};
font-family: Roboto, sans-serif;
font-size: 14px;
line-height: 20px;
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
}
a{
text-decoration: none;
color: {{colorPrimary}};
}
p, h1, h2, h3, h4, h5, h6, ul, ol{
margin-bottom: 8px;
margin-top: 0;
}
h1, h2{
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
h3, h4, h5, h6{
font-size: 14px;
line-height: 20px;
font-weight: 500;
}
b, strong{
font-weight: 500;
}
ul, ol{
padding-inline-start: 16px;
}
ul>li, ol>li{
padding-inline-start: 4px;
}
</style>
</head>
<body>
{{content}}
</body>
</html>

View File

@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
private static HashSet<Callback> callbacks=new HashSet<>(); private static HashSet<Callback> callbacks=new HashSet<>();
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged; private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
private boolean resumeAfterAudioFocusGain; private boolean resumeAfterAudioFocusGain;
private boolean isBuffering=true;
private BroadcastReceiver receiver=new BroadcastReceiver(){ private BroadcastReceiver receiver=new BroadcastReceiver(){
@Override @Override
@@ -169,13 +169,15 @@ public class AudioPlayerService extends Service{
} }
updateNotification(false, false); updateNotification(false, false);
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); int audiofocus = GlobalUserPreferences.overlayMedia ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN;
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, audiofocus);
player=new MediaPlayer(); player=new MediaPlayer();
player.setOnPreparedListener(this::onPlayerPrepared); player.setOnPreparedListener(this::onPlayerPrepared);
player.setOnErrorListener(this::onPlayerError); player.setOnErrorListener(this::onPlayerError);
player.setOnCompletionListener(this::onPlayerCompletion); player.setOnCompletionListener(this::onPlayerCompletion);
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted); player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
player.setOnInfoListener(this::onPlayerInfo);
try{ try{
player.setDataSource(this, Uri.parse(attachment.url)); player.setDataSource(this, Uri.parse(attachment.url));
player.prepareAsync(); player.prepareAsync();
@@ -187,7 +189,9 @@ public class AudioPlayerService extends Service{
} }
private void onPlayerPrepared(MediaPlayer mp){ private void onPlayerPrepared(MediaPlayer mp){
Log.i(TAG, "onPlayerPrepared");
playerReady=true; playerReady=true;
isBuffering=false;
player.start(); player.start();
updateSessionState(false); updateSessionState(false);
} }
@@ -205,6 +209,21 @@ public class AudioPlayerService extends Service{
stopSelf(); stopSelf();
} }
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
switch(what){
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
isBuffering=true;
updateSessionState(false);
}
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
isBuffering=false;
updateSessionState(false);
}
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
}
return true;
}
private void onAudioFocusChanged(int change){ private void onAudioFocusChanged(int change){
switch(change){ switch(change){
case AudioManager.AUDIOFOCUS_LOSS -> { case AudioManager.AUDIOFOCUS_LOSS -> {
@@ -212,7 +231,7 @@ public class AudioPlayerService extends Service{
pause(false); pause(false);
} }
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
resumeAfterAudioFocusGain=true; resumeAfterAudioFocusGain=isPlaying();
pause(false); pause(false);
} }
case AudioManager.AUDIOFOCUS_GAIN -> { case AudioManager.AUDIOFOCUS_GAIN -> {
@@ -232,12 +251,16 @@ public class AudioPlayerService extends Service{
private void updateSessionState(boolean removeNotification){ private void updateSessionState(boolean removeNotification){
session.setPlaybackState(new PlaybackState.Builder() session.setPlaybackState(new PlaybackState.Builder()
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f) .setState(switch(getPlayState()){
case PLAYING -> PlaybackState.STATE_PLAYING;
case PAUSED -> PlaybackState.STATE_PAUSED;
case BUFFERING -> PlaybackState.STATE_BUFFERING;
}, player.getCurrentPosition(), 1f)
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO) .setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
.build()); .build());
updateNotification(!player.isPlaying(), removeNotification); updateNotification(!player.isPlaying(), removeNotification);
for(Callback cb:callbacks) for(Callback cb:callbacks)
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition()); cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
} }
private void updateNotification(boolean dismissable, boolean removeNotification){ private void updateNotification(boolean dismissable, boolean removeNotification){
@@ -310,6 +333,12 @@ public class AudioPlayerService extends Service{
return attachment.id; return attachment.id;
} }
public PlayState getPlayState(){
if(isBuffering)
return PlayState.BUFFERING;
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
}
public static void registerCallback(Callback cb){ public static void registerCallback(Callback cb){
callbacks.add(cb); callbacks.add(cb);
} }
@@ -333,7 +362,13 @@ public class AudioPlayerService extends Service{
} }
public interface Callback{ public interface Callback{
void onPlayStateChanged(String attachmentID, boolean playing, int position); void onPlayStateChanged(String attachmentID, PlayState state, int position);
void onPlaybackStopped(String attachmentID); void onPlaybackStopped(String attachmentID);
} }
public enum PlayState{
PLAYING,
PAUSED,
BUFFERING
}
} }

View File

@@ -4,142 +4,147 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.StringRes;
import android.os.Build; import android.os.Build;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.ContentType; import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.utils.ColorPalette; import org.joinmastodon.android.ui.utils.ColorPalette;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
public class GlobalUserPreferences{ public class GlobalUserPreferences{
private static final String TAG="GlobalUserPreferences";
public static boolean playGifs; public static boolean playGifs;
public static boolean useCustomTabs; public static boolean useCustomTabs;
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme;
// MEGALODON
public static boolean trueBlackTheme; public static boolean trueBlackTheme;
public static boolean showReplies;
public static boolean showBoosts;
public static boolean loadNewPosts; public static boolean loadNewPosts;
public static boolean showNewPostsButton; public static boolean showNewPostsButton;
public static boolean showInteractionCounts; public static boolean toolbarMarquee;
public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee;
public static boolean disableSwipe; public static boolean disableSwipe;
public static boolean showDividers;
public static boolean voteButtonForSingleChoice; public static boolean voteButtonForSingleChoice;
public static boolean enableDeleteNotifications; public static boolean enableDeleteNotifications;
public static boolean translateButtonOpenedOnly; public static boolean translateButtonOpenedOnly;
public static boolean uniformNotificationIcon; public static boolean uniformNotificationIcon;
public static boolean relocatePublishButton;
public static boolean reduceMotion; public static boolean reduceMotion;
public static boolean keepOnlyLatestNotification;
public static boolean disableAltTextReminder;
public static boolean showAltIndicator; public static boolean showAltIndicator;
public static boolean showNoAltIndicator; public static boolean showNoAltIndicator;
public static boolean enablePreReleases; public static boolean enablePreReleases;
public static PrefixRepliesMode prefixReplies; public static PrefixRepliesMode prefixReplies;
public static boolean bottomEncoding;
public static boolean collapseLongPosts; public static boolean collapseLongPosts;
public static boolean spectatorMode; public static boolean spectatorMode;
public static boolean autoHideFab; public static boolean autoHideFab;
public static boolean compactReblogReplyLine;
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static ColorPreference color;
public static boolean disableM3PillActiveIndicator;
public static boolean showNavigationLabels;
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
public static boolean overlayMedia;
// MOSHIDON
public static boolean showDividers;
public static boolean relocatePublishButton;
public static boolean defaultToUnlistedReplies; public static boolean defaultToUnlistedReplies;
public static boolean doubleTapToSwipe; public static boolean doubleTapToSwipe;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog; public static boolean confirmBeforeReblog;
public static boolean hapticFeedback; public static boolean hapticFeedback;
public static boolean replyLineAboveHeader; public static boolean replyLineAboveHeader;
public static boolean swapBookmarkWithBoostAction; public static boolean swapBookmarkWithBoostAction;
public static boolean loadRemoteAccountFollowers; public static boolean loadRemoteAccountFollowers;
public static boolean mentionRebloggerAutomatically; public static boolean mentionRebloggerAutomatically;
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode;
public static Set<String> accountsWithContentTypesEnabled;
public static Map<String, ContentType> accountsDefaultContentTypes;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
public static Map<String, Integer> recentEmojis;
/**
* Pleroma
*/
public static String replyVisibility;
public static SharedPreferences getPrefs(){ public static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
} }
private static <T> T fromJson(String json, Type type, T orElse) { public static <T> T fromJson(String json, Type type, T orElse){
if (json == null) return orElse; if(json==null) return orElse;
try { return gson.fromJson(json, type); } try{
catch (JsonSyntaxException ignored) { return orElse; } T value=gson.fromJson(json, type);
return value==null ? orElse : value;
}catch(JsonSyntaxException ignored){
return orElse;
}
} }
public static void removeAccount(String accountId) { public static <T extends Enum<T>> T enumValue(Class<T> enumType, String name) {
recentLanguages.remove(accountId); try { return Enum.valueOf(enumType, name); }
pinnedTimelines.remove(accountId); catch (NullPointerException npe) { return null; }
accountsInGlitchMode.remove(accountId);
accountsWithLocalOnlySupport.remove(accountId);
accountsWithContentTypesEnabled.remove(accountId);
accountsDefaultContentTypes.remove(accountId);
save();
} }
public static void load(){ public static void load(){
SharedPreferences prefs=getPrefs(); SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true); playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true); useCustomTabs=prefs.getBoolean("useCustomTabs", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
altTextReminders=prefs.getBoolean("altTextReminders", true);
confirmUnfollow=prefs.getBoolean("confirmUnfollow", true);
confirmBoost=prefs.getBoolean("confirmBoost", false);
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
// MEGALODON
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false); trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true); loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true); showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false); toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
disableSwipe=prefs.getBoolean("disableSwipe", false); disableSwipe=prefs.getBoolean("disableSwipe", false);
showDividers =prefs.getBoolean("showDividers", false);
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true); voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false); enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false); translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false); uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
reduceMotion=prefs.getBoolean("reduceMotion", false); reduceMotion=prefs.getBoolean("reduceMotion", false);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
showAltIndicator=prefs.getBoolean("showAltIndicator", true); showAltIndicator=prefs.getBoolean("showAltIndicator", true);
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true); showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
enablePreReleases=prefs.getBoolean("enablePreReleases", false); enablePreReleases=prefs.getBoolean("enablePreReleases", false);
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name())); prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false); spectatorMode=prefs.getBoolean("spectatorMode", false);
autoHideFab=prefs.getBoolean("autoHideFab", true); autoHideFab=prefs.getBoolean("autoHideFab", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true); compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
overlayMedia=prefs.getBoolean("overlayMedia", false);
// MOSHIDON
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
showDividers =prefs.getBoolean("showDividers", false);
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false); defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true); doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true); replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false); confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
hapticFeedback=prefs.getBoolean("hapticFeedback", true); hapticFeedback=prefs.getBoolean("hapticFeedback", true);
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false); swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
@@ -156,9 +161,8 @@ public class GlobalUserPreferences{
replyVisibility=prefs.getString("replyVisibility", null); replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>()); accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>()); accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
if (prefs.contains("prefixRepliesWithRe")) { if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false) prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
@@ -179,30 +183,30 @@ public class GlobalUserPreferences{
// invalid color name or color was previously saved as integer // invalid color name or color was previously saved as integer
color=ColorPreference.PURPLE; color=ColorPreference.PURPLE;
} }
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
} }
public static void save(){ public static void save(){
getPrefs().edit() getPrefs().edit()
.putBoolean("playGifs", playGifs) .putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs) .putBoolean("useCustomTabs", useCustomTabs)
.putBoolean("showReplies", showReplies) .putInt("theme", theme.ordinal())
.putBoolean("showBoosts", showBoosts) .putBoolean("altTextReminders", altTextReminders)
.putBoolean("confirmUnfollow", confirmUnfollow)
.putBoolean("confirmBoost", confirmBoost)
.putBoolean("confirmDeletePost", confirmDeletePost)
// MEGALODON
.putBoolean("loadNewPosts", loadNewPosts) .putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("showNewPostsButton", showNewPostsButton) .putBoolean("showNewPostsButton", showNewPostsButton)
.putBoolean("trueBlackTheme", trueBlackTheme) .putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts) .putBoolean("toolbarMarquee", toolbarMarquee)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
.putBoolean("disableMarquee", disableMarquee)
.putBoolean("disableSwipe", disableSwipe) .putBoolean("disableSwipe", disableSwipe)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications) .putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly) .putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
.putBoolean("showDividers", showDividers)
.putBoolean("relocatePublishButton", relocatePublishButton)
.putBoolean("uniformNotificationIcon", uniformNotificationIcon) .putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("reduceMotion", reduceMotion) .putBoolean("reduceMotion", reduceMotion)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("disableAltTextReminder", disableAltTextReminder)
.putBoolean("showAltIndicator", showAltIndicator) .putBoolean("showAltIndicator", showAltIndicator)
.putBoolean("showNoAltIndicator", showNoAltIndicator) .putBoolean("showNoAltIndicator", showNoAltIndicator)
.putBoolean("enablePreReleases", enablePreReleases) .putBoolean("enablePreReleases", enablePreReleases)
@@ -211,6 +215,26 @@ public class GlobalUserPreferences{
.putBoolean("spectatorMode", spectatorMode) .putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab) .putBoolean("autoHideFab", autoHideFab)
.putBoolean("compactReblogReplyLine", compactReblogReplyLine) .putBoolean("compactReblogReplyLine", compactReblogReplyLine)
.putString("color", color.name())
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
.putBoolean("showNavigationLabels", showNavigationLabels)
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
.putBoolean("overlayMedia", overlayMedia)
// MOSHIDON
.putString("recentLanguages", gson.toJson(recentLanguages))
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
.putString("recentEmojis", gson.toJson(recentEmojis))
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putString("publishButtonText", publishButtonText) .putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding) .putBoolean("bottomEncoding", bottomEncoding)
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies) .putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
@@ -221,22 +245,64 @@ public class GlobalUserPreferences{
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction) .putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers) .putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically) .putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
.putBoolean("showDividers", showDividers)
.putBoolean("relocatePublishButton", relocatePublishButton)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putInt("theme", theme.ordinal()) .putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
.putString("recentEmojis", gson.toJson(recentEmojis))
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
.apply(); .apply();
} }
private static void migrateToUpstreamVersion61(){
Log.d(TAG, "Migrating preferences to upstream version 61!!");
Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
Type pinnedTimelinesType = new TypeToken<Map<String, ArrayList<TimelineDefinition>>>() {}.getType();
Type recentLanguagesType = new TypeToken<Map<String, ArrayList<String>>>() {}.getType();
// migrate global preferences
SharedPreferences prefs=getPrefs();
altTextReminders=!prefs.getBoolean("disableAltTextReminder", false);
confirmBoost=prefs.getBoolean("confirmBeforeReblog", false);
toolbarMarquee=!prefs.getBoolean("disableMarquee", false);
save();
// migrate local preferences
AccountSessionManager asm=AccountSessionManager.getInstance();
// reset: Set<String> accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
Map<String, ContentType> accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
Map<String, ArrayList<TimelineDefinition>> pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
Set<String> accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
Set<String> accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
Map<String, ArrayList<String>> recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
for(AccountSession session : asm.getLoggedInAccounts()){
String accountID=session.getID();
AccountLocalPreferences localPrefs=session.getLocalPreferences();
localPrefs.revealCWs=prefs.getBoolean("alwaysExpandContentWarnings", false);
localPrefs.recentLanguages=recentLanguages.get(accountID);
// reset: localPrefs.contentTypesEnabled=accountsWithContentTypesEnabled.contains(accountID);
localPrefs.defaultContentType=accountsDefaultContentTypes.getOrDefault(accountID, ContentType.PLAIN);
localPrefs.showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
localPrefs.timelines=pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
localPrefs.localOnlySupported=accountsWithLocalOnlySupport.contains(accountID);
localPrefs.glitchInstance=accountsInGlitchMode.contains(accountID);
localPrefs.publishButtonText=prefs.getString("publishButtonText", null);
localPrefs.keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
localPrefs.showReplies=prefs.getBoolean("showReplies", true);
localPrefs.showBoosts=prefs.getBoolean("showBoosts", true);
if(session.getInstance().map(Instance::isAkkoma).orElse(false)){
localPrefs.timelineReplyVisibility=prefs.getString("replyVisibility", null);
}
localPrefs.save();
}
prefs.edit().putInt("migrationLevel", 61).apply();
}
public enum ColorPreference{ public enum ColorPreference{
MATERIAL3, MATERIAL3,
PINK, PINK,
@@ -247,7 +313,22 @@ public class GlobalUserPreferences{
RED, RED,
YELLOW, YELLOW,
NORD, NORD,
WHITE WHITE;
public @StringRes int getName() {
return switch(this){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
case NORD -> R.string.mo_color_palette_nord;
case WHITE -> R.string.mo_color_palette_black_and_white;
};
}
} }
public enum ThemePreference{ public enum ThemePreference{

View File

@@ -11,6 +11,7 @@ import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore; import android.provider.MediaStore;
@@ -20,6 +21,7 @@ import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.TakePictureRequestEvent; import org.joinmastodon.android.events.TakePictureRequestEvent;
@@ -30,6 +32,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment; import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment; import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
@@ -37,6 +40,9 @@ import org.parceler.Parcels;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent { public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override @Override
@@ -82,6 +88,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
showFragmentForNotification(notification, session.getID()); showFragmentForNotification(notification, session.getID());
} else if (intent.getBooleanExtra("compose", false)){ } else if (intent.getBooleanExtra("compose", false)){
showCompose(); showCompose();
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
} else { } else {
showFragmentClearingBackStack(fragment); showFragmentClearingBackStack(fragment);
maybeRequestNotificationsPermission(); maybeRequestNotificationsPermission();
@@ -120,11 +128,55 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
} }
}else if(intent.getBooleanExtra("compose", false)){ }else if(intent.getBooleanExtra("compose", false)){
showCompose(); showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){ }/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this); GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/ }*/
} }
public void handleURL(Uri uri, String accountID){
if(uri==null)
return;
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
return;
AccountSession session;
if(accountID==null)
session=AccountSessionManager.getInstance().getLastActiveAccount();
else
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
Bundle args=new Bundle();
args.putString("account", accountID);
if(result.statuses!=null && !result.statuses.isEmpty()){
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
Nav.go(MainActivity.this, ThreadFragment.class, args);
}else if(result.accounts!=null && !result.accounts.isEmpty()){
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
Nav.go(MainActivity.this, ProfileFragment.class, args);
}else{
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(MainActivity.this);
}
})
.wrapProgress(this, progressText, true)
.exec(accountID);
}
private void showFragmentForNotification(Notification notification, String accountID){ private void showFragmentForNotification(Notification notification, String accountID){
try{ try{
notification.postprocess(); notification.postprocess();

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.webkit.WebView;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
@@ -28,5 +29,8 @@ public class MastodonApp extends Application{
PushSubscriptionManager.tryRegisterFCM(); PushSubscriptionManager.tryRegisterFCM();
GlobalUserPreferences.load(); GlobalUserPreferences.load();
if(BuildConfig.DEBUG){
WebView.setWebContentsDebuggingEnabled(true);
}
} }
} }

View File

@@ -17,6 +17,7 @@ import android.graphics.drawable.Drawable;
import android.opengl.Visibility; import android.opengl.Visibility;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@@ -29,7 +30,6 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationAction; import org.joinmastodon.android.model.NotificationAction;
@@ -38,12 +38,15 @@ import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -62,6 +65,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
private static final int SUMMARY_ID = 791; private static final int SUMMARY_ID = 791;
private static int notificationId = 0; private static int notificationId = 0;
private static final Map<String, Integer> notificationIdsForAccounts = new HashMap<>();
@Override @Override
public void onReceive(Context context, Intent intent){ public void onReceive(Context context, Intent intent){
@@ -93,9 +97,12 @@ public class PushNotificationReceiver extends BroadcastReceiver{
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found"); Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
return; return;
} }
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
return;
}
String accountID=account.getID(); String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s); PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
E.post(new NotificationReceivedEvent(accountID, pn.notificationId+""));
new GetNotificationByID(pn.notificationId+"") new GetNotificationByID(pn.notificationId+"")
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
@@ -128,24 +135,16 @@ public class PushNotificationReceiver extends BroadcastReceiver{
if(intent.hasExtra("notification")){ if(intent.hasExtra("notification")){
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification")); org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
String statusID = null; String statusID=notification.status.id;
String targetAccountID = null; if (statusID != null) {
if(notification.status != null){
statusID = notification.status.id;
}
if(notification.account != null){
targetAccountID = notification.account.id;
}
if (statusID != null || targetAccountID != null) {
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance(); AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
Preferences preferences = accountSessionManager.getAccount(accountID).preferences; Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
switch (NotificationAction.values()[intent.getIntExtra("notificationAction", 0)]) { switch (NotificationAction.values()[intent.getIntExtra("notificationAction", 0)]) {
case FAVORITE -> new SetStatusFavorited(statusID, true).exec(accountID); case FAVORITE -> new SetStatusFavorited(statusID, true).exec(accountID);
case BOOKMARK -> new SetStatusBookmarked(statusID, true).exec(accountID); case BOOKMARK -> new SetStatusBookmarked(statusID, true).exec(accountID);
case BOOST -> new SetStatusReblogged(notification.status.id, true, preferences.postingDefaultVisibility).exec(accountID); case REBLOG -> new SetStatusReblogged(notification.status.id, true, preferences.postingDefaultVisibility).exec(accountID);
case UNBOOST -> new SetStatusReblogged(notification.status.id, false, preferences.postingDefaultVisibility).exec(accountID); case UNDO_REBLOG -> new SetStatusReblogged(notification.status.id, false, preferences.postingDefaultVisibility).exec(accountID);
case REPLY -> handleReplyAction(context, accountID, intent, notification, notificationId, preferences); case REPLY -> handleReplyAction(context, accountID, intent, notification, notificationId, preferences);
case FOLLOW_BACK -> new SetAccountFollowed(notification.account.id, true, true, false).exec(accountID); case FOLLOW_BACK -> new SetAccountFollowed(notification.account.id, true, true, false).exec(accountID);
default -> Log.w(TAG, "onReceive: Failed to get NotificationAction"); default -> Log.w(TAG, "onReceive: Failed to get NotificationAction");
@@ -157,10 +156,15 @@ public class PushNotificationReceiver extends BroadcastReceiver{
} }
} }
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
}
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){ private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
NotificationManager nm=context.getSystemService(NotificationManager.class); NotificationManager nm=context.getSystemService(NotificationManager.class);
notificationId=getPrefs().getInt("latestNotificationId", 0); AccountSession session=AccountSessionManager.get(accountID);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self; Account self=session.self;
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain; String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
Notification.Builder builder; Notification.Builder builder;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
@@ -230,8 +234,21 @@ public class PushNotificationReceiver extends BroadcastReceiver{
builder.setSubText(accountName); builder.setSubText(accountName);
} }
int id = GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : notificationId++; int id;
getPrefs().edit().putInt("latestNotificationId", notificationId).apply(); if(session.getLocalPreferences().keepOnlyLatestNotification){
if(notificationIdsForAccounts.containsKey(accountID)){
// we overwrite the existing notification
id=notificationIdsForAccounts.get(accountID);
}else{
// there's no existing notification, so we increment
id=notificationId++;
// and store the notification id for this account
notificationIdsForAccounts.put(accountID, id);
}
}else{
// we don't want to overwrite anything, therefore incrementing
id=notificationId++;
}
if (notification != null){ if (notification != null){
switch (pn.notificationType){ switch (pn.notificationType){

View File

@@ -0,0 +1,81 @@
package org.joinmastodon.android;
import android.content.Context;
import android.util.Log;
import org.jetbrains.annotations.NotNull;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.unifiedpush.android.connector.MessagingReceiver;
import java.util.List;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class UnifiedPushNotificationReceiver extends MessagingReceiver{
private static final String TAG="UnifiedPushNotificationReceiver";
public UnifiedPushNotificationReceiver() {
super();
}
@Override
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
// Called when a new endpoint be used for sending push messages
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null);
}
@Override
public void onRegistrationFailed(@NotNull Context context, @NotNull String instance) {
// called when the registration is not possible, eg. no network
Log.d(TAG, "onRegistrationFailed: " + instance);
//re-register for gcm
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null);
}
@Override
public void onUnregistered(@NotNull Context context, @NotNull String instance) {
// called when this application is unregistered from receiving push messages
Log.d(TAG, "onUnregistered: " + instance);
//re-register for gcm
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null);
}
@Override
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
// Called when a new message is received. The message contains the full POST body of the push message
AccountSession account = AccountSessionManager.getInstance().getAccount(instance);
//this is stupid
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on
// The official uses fcm and moves the headers to extra data, see
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540
account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
result.items
.stream()
.findFirst()
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
}
@Override
public void onError(ErrorResponse error){
//professional error handling
}
});
}
}

View File

@@ -13,22 +13,20 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@@ -43,6 +41,8 @@ public class CacheController{
private final String accountID; private final String accountID;
private DatabaseHelper db; private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase; private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private static final int POST_FLAG_GAP_AFTER=1; private static final int POST_FLAG_GAP_AFTER=1;
@@ -58,7 +58,6 @@ public class CacheController{
cancelDelayedClose(); cancelDelayedClose();
databaseThread.postRunnable(()->{ databaseThread.postRunnable(()->{
try{ try{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){ if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase(); SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){ try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
@@ -66,18 +65,16 @@ public class CacheController{
ArrayList<Status> result=new ArrayList<>(); ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst(); cursor.moveToFirst();
String newMaxID; String newMaxID;
outer:
do{ do{
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class); Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
status.postprocess(); status.postprocess();
int flags=cursor.getInt(1); int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0); status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id; newMaxID=status.id;
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
continue outer;
result.add(status); result.add(status);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true))); uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return; return;
} }
@@ -85,11 +82,13 @@ public class CacheController{
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x); Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
} }
} }
new GetHomeTimeline(maxID, null, count, null) new GetHomeTimeline(maxID, null, count, null, AccountSessionManager.get(accountID).getLocalPreferences().timelineReplyVisibility)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); ArrayList<Status> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null); putHomeTimeline(result, maxID==null);
} }
@@ -126,12 +125,39 @@ public class CacheController{
}); });
} }
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){ public void updateStatus(Status status) {
runOnDbThread((db)->{
ContentValues statusUpdate=new ContentValues(1);
statusUpdate.put("json", MastodonAPIController.gson.toJson(status));
db.update("home_timeline", statusUpdate, "id = ?", new String[] { status.id });
});
}
public void updateNotification(Notification notification) {
runOnDbThread((db)->{
ContentValues notificationUpdate=new ContentValues(1);
notificationUpdate.put("json", MastodonAPIController.gson.toJson(notification));
String[] notificationArgs = new String[] { notification.id };
db.update("notifications_all", notificationUpdate, "id = ?", notificationArgs);
db.update("notifications_mentions", notificationUpdate, "id = ?", notificationArgs);
db.update("notifications_posts", notificationUpdate, "id = ?", notificationArgs);
ContentValues statusUpdate=new ContentValues(1);
statusUpdate.put("json", MastodonAPIController.gson.toJson(notification.status));
db.update("home_timeline", statusUpdate, "id = ?", new String[] { notification.status.id });
});
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
cancelDelayedClose(); cancelDelayedClose();
databaseThread.postRunnable(()->{ databaseThread.postRunnable(()->{
try{ try{
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID); if(!onlyMentions && !onlyPosts && loadingNotifications){
List<Filter> filters=accountSession.wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList()); synchronized(pendingNotificationsCallbacks){
pendingNotificationsCallbacks.add(callback);
}
return;
}
if(!forceReload){ if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase(); SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all"; String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
@@ -140,42 +166,56 @@ public class CacheController{
ArrayList<Notification> result=new ArrayList<>(); ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst(); cursor.moveToFirst();
String newMaxID; String newMaxID;
outer:
do{ do{
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class); Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
ntf.postprocess(); ntf.postprocess();
newMaxID=ntf.id; newMaxID=ntf.id;
if(ntf.status!=null){
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
continue outer;
}
result.add(ntf); result.add(ntf);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true))); AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
return; return;
} }
}catch(IOException x){ }catch(IOException x){
Log.w(TAG, "getNotifications: corrupted notification object in database", x); Log.w(TAG, "getNotifications: corrupted notification object in database", x);
} }
} }
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); if(!onlyMentions && !onlyPosts)
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma()) loadingNotifications=true;
boolean isAkkoma = AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), isAkkoma)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Notification> result){ public void onSuccess(List<Notification> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{ ArrayList<Notification> filtered=new ArrayList<>(result);
if(ntf.status!=null){ AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status); PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
} callback.onSuccess(res);
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
putNotifications(result, onlyMentions, onlyPosts, maxID==null); putNotifications(result, onlyMentions, onlyPosts, maxID==null);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
callback.onError(error); callback.onError(error);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onError(error);
}
pendingNotificationsCallbacks.clear();
}
}
} }
}) })
.exec(accountID); .exec(accountID);
@@ -327,7 +367,7 @@ public class CacheController{
createRecentSearchesTable(db); createRecentSearchesTable(db);
} }
if(oldVersion<3){ if(oldVersion<3){
// MEGALODON-SPECIFIC // MEGALODON
createPostsNotificationsTable(db); createPostsNotificationsTable(db);
} }
if(oldVersion<4){ if(oldVersion<4){

View File

@@ -117,6 +117,9 @@ public class MastodonAPIController{
synchronized(req){ synchronized(req){
req.okhttpCall=call; req.okhttpCall=call;
} }
if(req.timeout>0){
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
}
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
@@ -153,13 +156,17 @@ public class MastodonAPIController{
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null) if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType()); respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else else if(req.respClass!=null)
respObj=gson.fromJson(respJson, req.respClass); respObj=gson.fromJson(respJson, req.respClass);
else
respObj=null;
}else{ }else{
if(req.respTypeToken!=null) if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType()); respObj=gson.fromJson(reader, req.respTypeToken.getType());
else else if(req.respClass!=null)
respObj=gson.fromJson(reader, req.respClass); respObj=gson.fromJson(reader, req.respClass);
else
respObj=null;
} }
}catch(JsonIOException|JsonSyntaxException x){ }catch(JsonIOException|JsonSyntaxException x){
if (req.context != null && response.body().contentType().subtype().equals("html")) { if (req.context != null && response.body().contentType().subtype().equals("html")) {

View File

@@ -49,6 +49,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Token token; Token token;
boolean canceled, isRemote; boolean canceled, isRemote;
Map<String, String> headers; Map<String, String> headers;
long timeout;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems; protected boolean removeUnsupportedItems;
@Nullable Context context; @Nullable Context context;
@@ -117,16 +118,16 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
.findAny()) .findAny())
.map(AccountSession::getID) .map(AccountSession::getID)
.map(this::exec) .map(this::exec)
.orElse(this.execNoAuth(domain)); .orElseGet(() -> this.execNoAuth(domain));
} }
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){ public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null); return wrapProgress(context, message, cancelable, null);
} }
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){ public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
progressDialog=new ProgressDialog(activity); progressDialog=new ProgressDialog(context);
progressDialog.setMessage(activity.getString(message)); progressDialog.setMessage(context.getString(message));
progressDialog.setCancelable(cancelable); progressDialog.setCancelable(cancelable);
if (transform != null) transform.accept(progressDialog); if (transform != null) transform.accept(progressDialog);
if(cancelable){ if(cancelable){
@@ -152,6 +153,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
headers.put(key, value); headers.put(key, value);
} }
protected void setTimeout(long timeout){
this.timeout=timeout;
}
protected String getPathPrefix(){ protected String getPathPrefix(){
return "/api/v1"; return "/api/v1";
} }

View File

@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
private String accountID; private String accountID;
private PrivateKey privateKey; private PrivateKey privateKey;
private PublicKey publicKey; private PublicKey publicKey;
private PublicKey serverKey;
private byte[] authKey; private byte[] authKey;
public PushSubscriptionManager(String accountID){ public PushSubscriptionManager(String accountID){
@@ -121,9 +120,22 @@ public class PushSubscriptionManager{
return !TextUtils.isEmpty(deviceToken); return !TextUtils.isEmpty(deviceToken);
} }
public void registerAccountForPush(PushSubscription subscription){ public void registerAccountForPush(PushSubscription subscription){
// this function is used for registering push notifications using FCM
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease"))
return;
if(TextUtils.isEmpty(deviceToken)) if(TextUtils.isEmpty(deviceToken))
throw new IllegalStateException("No device push token available"); throw new IllegalStateException("No device push token available");
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
registerAccountForPush(subscription, endpoint);
}
public void registerAccountForPush(PushSubscription subscription, String endpoint){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
Log.d(TAG, "registerAccountForPush: started for "+accountID); Log.d(TAG, "registerAccountForPush: started for "+accountID);
String encodedPublicKey, encodedAuthKey, pushAccountID; String encodedPublicKey, encodedAuthKey, pushAccountID;
@@ -152,20 +164,15 @@ public class PushSubscriptionManager{
Log.e(TAG, "registerAccountForPush: error generating encryption key", e); Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
return; return;
} }
new RegisterForPushNotifications(deviceToken, new RegisterForPushNotifications(endpoint,
encodedPublicKey, encodedPublicKey,
encodedAuthKey, encodedAuthKey,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts, subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
subscription==null ? PushSubscription.Policy.ALL : subscription.policy, subscription==null ? PushSubscription.Policy.ALL : subscription.policy)
pushAccountID)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(PushSubscription result){ public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID); AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null) if(session==null)
return; return;

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.api;
import com.google.gson.reflect.TypeToken;
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
super(method, path, (Class<Void>)null);
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
import java.util.List;
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
public GetAccountFeaturedHashtags(String id){
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
}
}

View File

@@ -21,22 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){ switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true"); case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {} case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true"); case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> { case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true"); addQueryParameter("exclude_replies", "true");
addQueryParameter("exclude_reblogs", "true"); addQueryParameter("exclude_reblogs", "true");
} }
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true"); case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
case PINNED -> addQueryParameter("pinned", "true");
} }
} }
public enum Filter{ public enum Filter{
DEFAULT, DEFAULT,
INCLUDE_REPLIES, INCLUDE_REPLIES,
PINNED,
MEDIA, MEDIA,
NO_REBLOGS, NO_REBLOGS,
OWN_POSTS_AND_REPLIES OWN_POSTS_AND_REPLIES,
PINNED
} }
} }

View File

@@ -4,21 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{ public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason){ public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
super(HttpMethod.POST, "/accounts", Token.class); super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason)); setRequestBody(new Body(username, email, password, locale, reason, timezone));
} }
private static class Body{ private static class Body{
public String username, email, password, locale, reason; public String username, email, password, locale, reason, timeZone;
public boolean agreement=true; public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason){ public Body(String username, String email, String password, String locale, String reason, String timeZone){
this.username=username; this.username=username;
this.email=email; this.email=email;
this.password=password; this.password=password;
this.locale=locale; this.locale=locale;
this.reason=reason; this.reason=reason;
this.timeZone=timeZone;
} }
} }
} }

View File

@@ -6,7 +6,7 @@ import org.joinmastodon.android.model.Relationship;
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{ public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
public SetAccountMuted(String id, boolean muted, long duration){ public SetAccountMuted(String id, boolean muted, long duration){
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class); super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
setRequestBody(muted ? new Request(duration): new Object()); setRequestBody(new Request(duration));
} }
private static class Request{ private static class Request{

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.StatusPrivacy;
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
}
private static class Request{
public Boolean locked, discoverable;
public RequestSource source;
public Request(Boolean locked, Boolean discoverable, RequestSource source){
this.locked=locked;
this.discoverable=discoverable;
this.source=source;
}
}
private static class RequestSource{
public StatusPrivacy privacy;
public String language;
public RequestSource(StatusPrivacy privacy, String language){
this.privacy=privacy;
this.language=language;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import java.util.List;
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
public GetCatalogDefaultInstances(){
super(HttpMethod.GET, null, new TypeToken<>(){});
setTimeout(500);
}
@Override
public Uri getURL(){
return Uri.parse("https://api.joinmastodon.org/default-servers");
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
public class CreateFilter extends MastodonAPIRequest<Filter>{
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
super(HttpMethod.POST, "/filters", Filter.class);
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class DeleteFilter extends ResultlessMastodonAPIRequest{
public DeleteFilter(String id){
super(HttpMethod.DELETE, "/filters/"+id);
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import java.util.EnumSet;
import java.util.List;
class FilterRequest{
public String title;
public EnumSet<FilterContext> context;
public FilterAction filterAction;
public Integer expiresIn;
public List<KeywordAttribute> keywordsAttributes;
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
this.title=title;
this.context=context;
this.filterAction=filterAction;
this.expiresIn=expiresIn;
this.keywordsAttributes=keywordsAttributes;
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.api.requests.accounts; package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
import java.util.List; import java.util.List;
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{ public class GetFilters extends MastodonAPIRequest<List<Filter>>{
public GetWordFilters(){ public GetFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){}); super(HttpMethod.GET, "/filters", new TypeToken<>(){});
} }
@Override
protected String getPathPrefix(){
return "/api/v2";
}
} }

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.LegacyFilter;
import java.util.List;
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
public GetLegacyFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.annotations.SerializedName;
class KeywordAttribute{
public String id;
@SerializedName("_destroy")
public Boolean delete;
public String keyword;
public Boolean wholeWord;
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
this.id=id;
this.delete=delete;
this.keyword=keyword;
this.wholeWord=wholeWord;
}
}

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class UpdateFilter extends MastodonAPIRequest<Filter>{
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
List<KeywordAttribute> attrs=Stream.of(
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
).flatMap(Function.identity()).collect(Collectors.toList());
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.time.Instant;
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
public GetInstanceExtendedDescription(){
super(HttpMethod.GET, "/instance/extended_description", Response.class);
}
public static class Response{
public Instant updatedAt;
public String content;
}
}

View File

@@ -1,17 +1,12 @@
package org.joinmastodon.android.api.requests.markers; package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Marker; import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Markers;
import java.util.EnumSet; public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
public GetMarkers(){
public class GetMarkers extends MastodonAPIRequest<Markers> { super(HttpMethod.GET, "/markers", TimelineMarkers.class);
public GetMarkers(EnumSet<Marker.Type> timelines) { addQueryParameter("timeline[]", "home");
super(HttpMethod.GET, "/markers", Markers.class); addQueryParameter("timeline[]", "notifications");
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
addQueryParameter("timeline[]", type);
}
} }
} }

View File

@@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder; import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.model.Marker; import org.joinmastodon.android.model.TimelineMarkers;
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{ public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){ public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
super(HttpMethod.POST, "/markers", Response.class); super(HttpMethod.POST, "/markers", TimelineMarkers.class);
JsonObjectBuilder builder=new JsonObjectBuilder(); JsonObjectBuilder builder=new JsonObjectBuilder();
if(lastSeenHomePostID!=null) if(lastSeenHomePostID!=null)
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID)); builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID)); builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
setRequestBody(builder.build()); setRequestBody(builder.build());
} }
public static class Response{
public Marker home, notifications;
}
} }

View File

@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy, String accountID){ public RegisterForPushNotifications(String endpoint, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.POST, "/push/subscription", PushSubscription.class); super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
Request r=new Request(); Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID; r.subscription.endpoint=endpoint;
r.data.alerts=alerts; r.data.alerts=alerts;
r.policy=policy; r.policy=policy;
r.subscription.keys.p256dh=encryptionKey; r.subscription.keys.p256dh=encryptionKey;

View File

@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
addQueryParameter("resolve", "true"); addQueryParameter("resolve", "true");
} }
public GetSearchResults limit(int limit){
addQueryParameter("limit", String.valueOf(limit));
return this;
}
@Override @Override
protected String getPathPrefix(){ protected String getPathPrefix(){
return "/api/v2"; return "/api/v2";

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class AddStatusReaction extends MastodonAPIRequest<Status> {
public AddStatusReaction(String id, String emoji) {
super(HttpMethod.POST, "/statuses/" + id + "/react/" + emoji, Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class DeleteStatusReaction extends MastodonAPIRequest<Status> {
public DeleteStatusReaction(String id, String emoji) {
super(HttpMethod.POST, "/statuses/" + id + "/unreact/" + emoji, Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class PleromaAddStatusReaction extends MastodonAPIRequest<Status> {
public PleromaAddStatusReaction(String id, String emoji) {
super(HttpMethod.PUT, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class PleromaDeleteStatusReaction extends MastodonAPIRequest<Status> {
public PleromaDeleteStatusReaction(String id, String emoji) {
super(HttpMethod.DELETE, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.EmojiReaction;
import java.util.List;
public class PleromaGetStatusReactions extends MastodonAPIRequest<List<EmojiReaction>> {
public PleromaGetStatusReactions(String id, String emoji) {
super(HttpMethod.GET, "/pleroma/statuses/" + id + "/reactions/" + (emoji != null ? emoji : ""), new TypeToken<>(){});
}
}

View File

@@ -4,20 +4,19 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> { public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
public GetBubbleTimeline(String maxID, int limit) { public GetBubbleTimeline(String maxID, int limit, String replyVisibility) {
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID)) if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
if(limit>0) if(limit>0)
addQueryParameter("limit", limit+""); addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null) if(replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); addQueryParameter("reply_visibility", replyVisibility);
} }
} }

View File

@@ -2,16 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import android.text.TextUtils;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly){ public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly, String replyVisibility){
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
if (localOnly) if (localOnly)
addQueryParameter("local", "true"); addQueryParameter("local", "true");
@@ -30,17 +27,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
if(containsNone!=null) if(containsNone!=null)
for (String tag : containsNone) for (String tag : containsNone)
addQueryParameter("none[]", tag); addQueryParameter("none[]", tag);
} if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit){
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
} }
} }

View File

@@ -2,14 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){ public GetHomeTimeline(String maxID, String minID, int limit, String sinceID, String replyVisibility){
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
if(maxID!=null) if(maxID!=null)
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
@@ -19,7 +18,7 @@ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("since_id", sinceID); addQueryParameter("since_id", sinceID);
if(limit>0) if(limit>0)
addQueryParameter("limit", ""+limit); addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null) if(replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); addQueryParameter("reply_visibility", replyVisibility);
} }
} }

View File

@@ -2,14 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetListTimeline extends MastodonAPIRequest<List<Status>> { public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) { public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID, String replyVisibility) {
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
if(maxID!=null) if(maxID!=null)
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
@@ -19,7 +18,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
addQueryParameter("limit", ""+limit); addQueryParameter("limit", ""+limit);
if(sinceID!=null) if(sinceID!=null)
addQueryParameter("since_id", sinceID); addQueryParameter("since_id", sinceID);
if(GlobalUserPreferences.replyVisibility != null) if(replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); addQueryParameter("reply_visibility", replyVisibility);
} }
} }

View File

@@ -4,14 +4,13 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){ public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
if(local) if(local)
addQueryParameter("local", "true"); addQueryParameter("local", "true");
@@ -21,7 +20,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
if(limit>0) if(limit>0)
addQueryParameter("limit", limit+""); addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null) if(replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); addQueryParameter("reply_visibility", replyVisibility);
} }
} }

View File

@@ -0,0 +1,105 @@
package org.joinmastodon.android.api.session;
import static org.joinmastodon.android.GlobalUserPreferences.fromJson;
import static org.joinmastodon.android.GlobalUserPreferences.enumValue;
import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.SharedPreferences;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class AccountLocalPreferences{
private final SharedPreferences prefs;
public boolean showInteractionCounts;
public boolean customEmojiInNames;
public boolean revealCWs;
public boolean hideSensitiveMedia;
public boolean serverSideFiltersSupported;
// MEGALODON
public boolean showReplies;
public boolean showBoosts;
public ArrayList<String> recentLanguages;
public boolean bottomEncoding;
public ContentType defaultContentType;
public boolean contentTypesEnabled;
public ArrayList<TimelineDefinition> timelines;
public boolean localOnlySupported;
public boolean glitchInstance;
public String publishButtonText;
public String timelineReplyVisibility; // akkoma-only
public boolean keepOnlyLatestNotification;
public boolean emojiReactionsEnabled;
public boolean showEmojiReactionsInLists;
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
this.prefs=prefs;
showInteractionCounts=prefs.getBoolean("interactionCounts", false);
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
revealCWs=prefs.getBoolean("revealCWs", false);
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
// MEGALODON
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
glitchInstance=prefs.getBoolean("glitchInstance", false);
publishButtonText=prefs.getString("publishButtonText", null);
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
showEmojiReactionsInLists=prefs.getBoolean("showEmojiReactionsInLists", false);
}
public long getNotificationsPauseEndTime(){
return prefs.getLong("notificationsPauseTime", 0L);
}
public void setNotificationsPauseEndTime(long time){
prefs.edit().putLong("notificationsPauseTime", time).apply();
}
public void save(){
prefs.edit()
.putBoolean("interactionCounts", showInteractionCounts)
.putBoolean("emojiInNames", customEmojiInNames)
.putBoolean("revealCWs", revealCWs)
.putBoolean("hideSensitive", hideSensitiveMedia)
.putBoolean("serverSideFilters", serverSideFiltersSupported)
// MEGALODON
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putString("recentLanguages", gson.toJson(recentLanguages))
.putBoolean("bottomEncoding", bottomEncoding)
.putString("defaultContentType", defaultContentType==null ? null : defaultContentType.name())
.putBoolean("contentTypesEnabled", contentTypesEnabled)
.putString("timelines", gson.toJson(timelines))
.putBoolean("localOnlySupported", localOnlySupported)
.putBoolean("glitchInstance", glitchInstance)
.putString("publishButtonText", publishButtonText)
.putString("timelineReplyVisibility", timelineReplyVisibility)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
.putBoolean("showEmojiReactionsInLists", showEmojiReactionsInLists)
.apply();
}
}

View File

@@ -1,25 +1,53 @@
package org.joinmastodon.android.api.session; package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class AccountSession{ public class AccountSession{
private static final String TAG="AccountSession";
public Token token; public Token token;
public Account self; public Account self;
public String domain; public String domain;
@@ -32,15 +60,17 @@ public class AccountSession{
public PushSubscription pushSubscription; public PushSubscription pushSubscription;
public boolean needUpdatePushSettings; public boolean needUpdatePushSettings;
public long filtersLastUpdated; public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>(); public List<LegacyFilter> wordFilters=new ArrayList<>();
public String pushAccountID; public String pushAccountID;
public Preferences preferences;
public AccountActivationInfo activationInfo; public AccountActivationInfo activationInfo;
public Markers markers; public Preferences preferences;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController; private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager; private transient PushSubscriptionManager pushSubscriptionManager;
private transient SharedPreferences prefs;
private transient boolean preferencesNeedSaving;
private transient AccountLocalPreferences localPreferences;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){ AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token; this.token=token;
@@ -58,10 +88,6 @@ public class AccountSession{
return domain+"_"+self.id; return domain+"_"+self.id;
} }
public String getFullUsername() {
return "@"+self.username+"@"+domain;
}
public MastodonAPIController getApiController(){ public MastodonAPIController getApiController(){
if(apiController==null) if(apiController==null)
apiController=new MastodonAPIController(this); apiController=new MastodonAPIController(this);
@@ -92,6 +118,188 @@ public class AccountSession{
return pushSubscriptionManager; return pushSubscriptionManager;
} }
public String getFullUsername(){
return '@'+self.username+'@'+domain;
}
public void preferencesFromAccountSource(Account account) {
if (account != null && account.source != null && preferences != null) {
if (account.source.privacy != null)
preferences.postingDefaultVisibility = account.source.privacy;
if (account.source.language != null)
preferences.postingDefaultLanguage = account.source.language;
}
}
public void reloadPreferences(Consumer<Preferences> callback){
new GetPreferences()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Preferences result){
preferences=result;
preferencesFromAccountSource(self);
if(callback!=null)
callback.accept(result);
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
}
})
.exec(getID());
}
public SharedPreferences getRawLocalPreferences(){
if(prefs==null)
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
return prefs;
}
public void reloadNotificationsMarker(Consumer<String> callback){
new GetMarkers()
.setCallback(new Callback<>(){
@Override
public void onSuccess(TimelineMarkers result){
if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
String id=result.notifications.lastReadId;
String lastKnown=getLastKnownNotificationsMarker();
if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
// Marker moved back -- previous marker update must have failed.
// Pretend it didn't happen and repeat the request.
id=lastKnown;
new SaveMarkers(null, id).exec(getID());
}
callback.accept(id);
setNotificationsMarker(id, false);
}
}
@Override
public void onError(ErrorResponse error){}
})
.exec(getID());
}
public String getLastKnownNotificationsMarker(){
return getRawLocalPreferences().getString("notificationsMarker", null);
}
public void setNotificationsMarker(String id, boolean clearUnread){
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
}
public void logOut(Activity activity, Runnable onDone){
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
AccountSessionManager.getInstance().removeAccount(getID());
onDone.run();
}
@Override
public void onError(ErrorResponse error){
AccountSessionManager.getInstance().removeAccount(getID());
onDone.run();
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(getID());
}
public void savePreferencesLater(){
preferencesNeedSaving=true;
}
public void savePreferencesIfPending(){
if(preferencesNeedSaving){
new UpdateAccountCredentialsPreferences(preferences, null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
preferencesNeedSaving=false;
self=result;
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
Log.e(TAG, "failed to save preferences: "+error);
}
})
.exec(getID());
}
}
public AccountLocalPreferences getLocalPreferences(){
if(localPreferences==null)
localPreferences=new AccountLocalPreferences(getRawLocalPreferences(), this);
return localPreferences;
}
public void filterStatuses(List<Status> statuses, FilterContext context){
filterStatuses(statuses, context, null);
}
public void filterStatuses(List<Status> statuses, FilterContext context, Account profile){
filterStatusContainingObjects(statuses, Function.identity(), context, profile);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
filterStatusContainingObjects(objects, extractor, context, null);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
if(getLocalPreferences().serverSideFiltersSupported){
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
if(s.filtered==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(FilterResult filter:s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;
}
return false;
});
return;
}
if(wordFilters==null)
return;
for(T obj:objects){
Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){
getLocalPreferences().serverSideFiltersSupported=true;
getLocalPreferences().save();
return;
}
}
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(LegacyFilter filter:wordFilters){
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
return true;
}
return false;
});
}
public Optional<Instance> getInstance() { public Optional<Instance> getInstance() {
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain)); return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
} }

View File

@@ -21,23 +21,18 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetPreferences; import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent; import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import java.io.File; import java.io.File;
@@ -50,10 +45,10 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -166,11 +161,19 @@ public class AccountSessionManager{
return session; return session;
} }
public static AccountSession get(String id){
return getInstance().getAccount(id);
}
@Nullable @Nullable
public AccountSession tryGetAccount(String id){ public AccountSession tryGetAccount(String id){
return sessions.get(id); return sessions.get(id);
} }
public static Optional<AccountSession> getOptional(String id) {
return Optional.ofNullable(getInstance().tryGetAccount(id));
}
@Nullable @Nullable
public AccountSession tryGetAccount(Account account) { public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id); return sessions.get(account.getDomainFromURL() + "_" + account.id);
@@ -203,13 +206,19 @@ public class AccountSessionManager{
AccountSession session=getAccount(id); AccountSession session=getAccount(id);
session.getCacheController().closeDatabase(); session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db"); MastodonApp.context.deleteDatabase(id+".db");
GlobalUserPreferences.removeAccount(id); MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
MastodonApp.context.deleteSharedPreferences(id);
}else{
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
}
sessions.remove(id); sessions.remove(id);
if(lastActiveAccountID.equals(id)){ if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty()) if(sessions.isEmpty())
lastActiveAccountID=null; lastActiveAccountID=null;
else else
lastActiveAccountID=getLoggedInAccounts().get(0).getID(); lastActiveAccountID=getLoggedInAccounts().get(0).getID();
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
} }
writeAccountsFile(); writeAccountsFile();
String domain=session.domain.toLowerCase(); String domain=session.domain.toLowerCase();
@@ -282,14 +291,13 @@ public class AccountSessionManager{
HashSet<String> domains=new HashSet<>(); HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){ for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase()); domains.add(session.domain.toLowerCase());
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){ if(session == activeSession || now-session.infoLastUpdated>24L*3600_000L){
updateSessionPreferences(session); session.reloadPreferences(null);
updateSessionLocalInfo(session); updateSessionLocalInfo(session);
} }
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){ if(session == activeSession || (session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L)){
updateSessionWordFilters(session); updateSessionWordFilters(session);
} }
updateSessionMarkers(session);
} }
if(loadedInstances){ if(loadedInstances){
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null); maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
@@ -300,20 +308,12 @@ public class AccountSessionManager{
long now=System.currentTimeMillis(); long now=System.currentTimeMillis();
for(String domain:domains){ for(String domain:domains){
Long lastUpdated=instancesLastUpdated.get(domain); Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){ if(domain.equals(activeDomain) || lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain); updateInstanceInfo(domain);
} }
} }
} }
private void preferencesFromSource(AccountSession session, Account account) {
if (account != null && account.source != null && session.preferences != null) {
if (account.source.privacy != null)
session.preferences.postingDefaultVisibility = account.source.privacy;
if (account.source.language != null)
session.preferences.postingDefaultLanguage = account.source.language;
}
}
private void updateSessionLocalInfo(AccountSession session){ private void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount() new GetOwnAccount()
@@ -322,39 +322,7 @@ public class AccountSessionManager{
public void onSuccess(Account result){ public void onSuccess(Account result){
session.self=result; session.self=result;
session.infoLastUpdated=System.currentTimeMillis(); session.infoLastUpdated=System.currentTimeMillis();
preferencesFromSource(session, result); session.preferencesFromAccountSource(result);
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){}
})
.exec(session.getID());
}
private void updateSessionPreferences(AccountSession session){
new GetPreferences().setCallback(new Callback<>() {
@Override
public void onSuccess(Preferences preferences) {
session.preferences=preferences;
preferencesFromSource(session, session.self);
}
@Override
public void onError(ErrorResponse error) {
session.preferences = new Preferences();
preferencesFromSource(session, session.self);
}
}).exec(session.getID());
}
private void updateSessionWordFilters(AccountSession session){
new GetWordFilters()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Filter> result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile(); writeAccountsFile();
} }
@@ -366,19 +334,22 @@ public class AccountSessionManager{
.exec(session.getID()); .exec(session.getID());
} }
private void updateSessionMarkers(AccountSession session) { private void updateSessionWordFilters(AccountSession session){
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() { new GetLegacyFilters()
.setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Markers markers) { public void onSuccess(List<LegacyFilter> result){
session.markers = markers; session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile(); writeAccountsFile();
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error){
} }
}).exec(session.getID()); })
.exec(session.getID());
} }
public void updateInstanceInfo(String domain){ public void updateInstanceInfo(String domain){

View File

@@ -1,4 +0,0 @@
package org.joinmastodon.android.events;
public class AllNotificationsSeenEvent {
}

View File

@@ -1,9 +0,0 @@
package org.joinmastodon.android.events;
public class NotificationReceivedEvent {
public String account, id;
public NotificationReceivedEvent(String account, String id) {
this.account = account;
this.id = id;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
public class NotificationsMarkerUpdatedEvent{
public final String accountID;
public final String marker;
public final boolean clearUnread;
public NotificationsMarkerUpdatedEvent(String accountID, String marker, boolean clearUnread){
this.accountID=accountID;
this.marker=marker;
this.clearUnread=clearUnread;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Filter;
public class SettingsFilterCreatedOrUpdatedEvent{
public final String accountID;
public final Filter filter;
public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
this.accountID=accountID;
this.filter=filter;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class SettingsFilterDeletedEvent{
public final String accountID;
public final String filterID;
public SettingsFilterDeletedEvent(String accountID, String filterID){
this.accountID=accountID;
this.filterID=filterID;
}
}

View File

@@ -1,14 +1,29 @@
package org.joinmastodon.android.events; package org.joinmastodon.android.events;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import java.util.ArrayList;
import java.util.List;
public class StatusCountersUpdatedEvent{ public class StatusCountersUpdatedEvent{
public String id; public String id;
public long favorites, reblogs, replies; public long favorites, reblogs, replies;
public boolean favorited, reblogged, bookmarked, pinned; public boolean favorited, reblogged, bookmarked, pinned;
public List<EmojiReaction> reactions;
public Status status;
public RecyclerView.ViewHolder viewHolder;
public StatusCountersUpdatedEvent(Status s){ public StatusCountersUpdatedEvent(Status s){
this(s, null);
}
public StatusCountersUpdatedEvent(Status s, RecyclerView.ViewHolder vh){
id=s.id; id=s.id;
status=s;
favorites=s.favouritesCount; favorites=s.favouritesCount;
reblogs=s.reblogsCount; reblogs=s.reblogsCount;
replies=s.repliesCount; replies=s.repliesCount;
@@ -16,5 +31,7 @@ public class StatusCountersUpdatedEvent{
reblogged=s.reblogged; reblogged=s.reblogged;
bookmarked=s.bookmarked; bookmarked=s.bookmarked;
pinned=s.pinned; pinned=s.pinned;
reactions=new ArrayList<>(s.reactions);
viewHolder=vh;
} }
} }

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class StatusDisplaySettingsChangedEvent{
public final String accountID;
public StatusDisplaySettingsChangedEvent(String accountID){
this.accountID=accountID;
}
}

View File

@@ -12,7 +12,7 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate; import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -48,27 +48,22 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
public void onAttach(Activity activity){ public void onAttach(Activity activity){
super.onAttach(activity);
user=Parcels.unwrap(getArguments().getParcelable("profileAccount")); user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter")); filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
super.onAttach(activity);
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
if(user==null) // TODO figure out why this happens
return;
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter) currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(getActivity()==null) return; if(getActivity()==null) return;
AccountSessionManager asm = AccountSessionManager.getInstance(); AccountSessionManager asm = AccountSessionManager.getInstance();
result=result.stream().filter(status -> { boolean empty=result.isEmpty();
// don't hide own posts in own profile AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true; onDataLoaded(result, !empty);
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
}).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
} }
}) })
.exec(accountID); .exec(accountID);
@@ -77,7 +72,7 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab = ((ProfileFragment) getParentFragment()).getFab(); view.setBackground(null); // prevents unnecessary overdraw
} }
@Override @Override
@@ -87,20 +82,20 @@ public class AccountTimelineFragment extends StatusListFragment{
loadData(); loadData();
} }
protected void onStatusCreated(StatusCreatedEvent ev){ protected void onStatusCreated(Status status){
AccountSessionManager asm = AccountSessionManager.getInstance(); AccountSessionManager asm = AccountSessionManager.getInstance();
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user)) if(!asm.isSelf(accountID, status.account) || !asm.isSelf(accountID, user))
return; return;
if(filter==GetAccountStatuses.Filter.PINNED) return; if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){ if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies // Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id)) if(status.inReplyToAccountId!=null && !status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return; return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){ }else if(filter==GetAccountStatuses.Filter.MEDIA){
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true)) if(Optional.ofNullable(status.mediaAttachments).map(List::isEmpty).orElse(true))
return; return;
} }
prependItems(Collections.singletonList(ev.status), true); prependItems(Collections.singletonList(status), true);
if (isOnTop()) scrollToTop(); if (isOnTop()) scrollToTop();
} }
@@ -131,8 +126,8 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return FilterContext.ACCOUNT;
} }
@Override @Override

View File

@@ -6,14 +6,10 @@ import android.content.res.Configuration;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Layout; import android.util.Log;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
@@ -30,6 +26,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote; import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
@@ -37,13 +34,17 @@ import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
@@ -58,6 +59,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -73,10 +75,11 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri { public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends MastodonRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>(); protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter; protected DisplayItemsAdapter adapter;
protected String accountID; protected String accountID;
@@ -101,7 +104,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(GlobalUserPreferences.disableMarquee){ if(GlobalUserPreferences.toolbarMarquee){
setTitleMarqueeEnabled(false); setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false); setSubtitleMarqueeEnabled(false);
} }
@@ -355,7 +358,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void getSelectorBounds(View view, Rect outRect){ public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false; boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1; int lastIndex = -1, firstIndex = -1;
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect); list.getDecoratedBoundsWithMargins(view, outRect);
}else{
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
RecyclerView.ViewHolder holder=list.getChildViewHolder(view); RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){ if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){ if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
@@ -432,6 +439,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
protected int getMainAdapterOffset(){ protected int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}
return 0; return 0;
} }
@@ -443,6 +453,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
c.drawLine(0, y, parent.getWidth(), y, paint); c.drawLine(0, y, parent.getWidth(), y, paint);
} }
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return false;
}
public abstract void onItemClick(String id); public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Status status, Poll poll){ protected void updatePoll(String itemID, Status status, Poll poll){
@@ -530,38 +544,57 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
.exec(accountID); .exec(accountID);
} }
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){ public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status; Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID()); toggleSpoiler(status, holder.getItemID());
} }
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){ public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
Status status=holder.getItem().status; Status status = holder.getItem().status;
revealSpoiler(status, holder.getItemID()); MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if (mediaGrid != null) {
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
else mediaGrid.hideSensitive();
} else {
// media grid's methods normally change the status' state - we still want to be able
// to do this if the media grid is not bound, tho - so, doing it ourselves here
status.sensitiveRevealed = !status.sensitiveRevealed;
}
holder.rebind();
}
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header != null) header.rebind();
}
protected void toggleSpoiler(Status status, String itemID){
status.spoilerRevealed=!status.spoilerRevealed;
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
status.sensitiveRevealed = false;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null)
spoiler.rebind();
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){
displayItems.addAll(index+1, spoilerItem.contentItems);
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
}else{
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
} }
protected void revealSpoiler(Status status, String itemID){
status.spoilerRevealed=true;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null) if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset()); adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null) if(header!=null)
header.rebind(); header.rebind();
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){ list.invalidateItemDecorations();
Status status=holder.getItem().status;
status.spoilerRevealed=!status.spoilerRevealed;
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
updateImagesSpoilerState(status, holder.getItemID());
} }
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) { public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
@@ -580,27 +613,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if (header != null) header.rebind(); if (header != null) header.rebind();
} }
protected void updateImagesSpoilerState(Status status, String itemID){ public void updateEmojiReactions(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>(); EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class);
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class); if(reactions != null){
if(mediaGrid!=null){ reactions.getItem().status.reactions.clear();
mediaGrid.setRevealed(status.spoilerRevealed); reactions.getItem().status.reactions.addAll(status.reactions);
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset()); reactions.rebind();
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
}
}
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
holder.rebind();
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
} }
} }
@@ -759,11 +777,28 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon())); assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
} }
public void rebuildAllDisplayItems(){
displayItems.clear();
for(T item:data){
displayItems.addAll(buildDisplayItems(item));
}
adapter.notifyDataSetChanged();
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
@Override @Override
protected void onDataLoaded(List<T> d, boolean more) { protected void onDataLoaded(List<T> d, boolean more) {
if(getContext()==null) return;
super.onDataLoaded(d, more); super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more // more available, but the page isn't even full yet? seems wrong, let's load some more
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem(); if(more && d.size() < itemsPerPage){
preloader.onScrolledToLastItem();
}
}
public void scrollBy(int x, int y) {
list.scrollBy(x, y);
} }
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -775,7 +810,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@NonNull @NonNull
@Override @Override
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent); BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent, BaseStatusListFragment.this);
onModifyItemViewHolder(holder);
return holder;
} }
@Override @Override
@@ -806,15 +843,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{ private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
private Paint dividerPaint=new Paint(), hiddenMediaPaint=new Paint(Paint.ANTI_ALIAS_FLAG); private Paint dividerPaint=new Paint();
private Typeface mediumTypeface=Typeface.create("sans-serif-medium", Typeface.NORMAL);
private Layout mediaHiddenTitleLayout, mediaHiddenTextLayout, tapToRevealTextLayout;
private int currentMediaHiddenLayoutsWidth=0;
{ {
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorPollVoted : R.attr.colorWindowBackground)); dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorM3OutlineVariant : R.attr.colorM3Surface));
dividerPaint.setStyle(Paint.Style.STROKE); dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1)); dividerPaint.setStrokeWidth(V.dp(0.5f));
} }
@Override @Override
@@ -824,80 +858,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
View bottomSibling=parent.getChildAt(i+1); View bottomSibling=parent.getChildAt(i+1);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling); RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh if(needDrawDivider(holder, siblingHolder)){
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint); drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
} }
} }
} }
@Override private boolean needDrawDivider(RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ if(needDividerForExtraItem(holder.itemView, siblingHolder.itemView, holder, siblingHolder))
for(int i=0;i<parent.getChildCount();i++){ return true;
View child=parent.getChildAt(i); if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); // Do not draw dividers between hashtag and/or account rows
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ return false;
hiddenMediaPaint.setColor(0x80000000); if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) return false;
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
} }
} return false;
}
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, UiUtils.MAX_WIDTH);
if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32));
c.save();
float totalHeight;
boolean hiddenByAuthor=imgHolder.getItem().status.sensitive;
if(hiddenByAuthor)
totalHeight=mediaHiddenTitleLayout.getHeight()+mediaHiddenTextLayout.getHeight()+V.dp(8);
else
totalHeight=tapToRevealTextLayout.getHeight();
c.translate(child.getX()+V.dp(16), child.getY()+child.getHeight()/2f-totalHeight/2f);
if(hiddenByAuthor){
mediaHiddenTitleLayout.draw(c);
c.translate(0, mediaHiddenTitleLayout.getHeight()+V.dp(8));
mediaHiddenTextLayout.draw(c);
}else{
tapToRevealTextLayout.draw(c);
}
c.restore();
}
}
}
}
}
private void rebuildMediaHiddenLayouts(int width){
currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content);
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
titlePaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray50));
titlePaint.setTextSize(V.dp(22));
titlePaint.setTypeface(mediumTypeface);
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
String tapToReveal=getString(R.string.tap_to_reveal);
tapToRevealTextLayout=StaticLayout.Builder.obtain(tapToReveal, 0, tapToReveal.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray200));
textPaint.setTextSize(V.dp(16));
String text=getString(R.string.sensitive_content_explain);
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(V.dp(5), 1f)
.build();
} }
} }
} }

View File

@@ -5,7 +5,8 @@ import android.net.Uri;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses; import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -39,8 +40,13 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
return Filter.FilterContext.ACCOUNT; // no-op
}
@Override
protected FilterContext getFilterContext() {
return FilterContext.ACCOUNT;
} }
@Override @Override

View File

@@ -1,10 +1,18 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.content.res.TypedArray; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Gravity; import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -12,28 +20,36 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.parceler.Parcels; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.utils.ColorPalette;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import me.grishka.appkit.Nav; import java.util.Collections;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import androidx.annotation.NonNull;
import me.grishka.appkit.fragments.ToolbarFragment; import androidx.annotation.Nullable;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
private static final String TAG="ComposeImageDescription";
private String accountID, attachmentID; private String accountID, attachmentID;
private EditText edit; private EditText edit;
private Button saveButton; private ImageView image;
private ContextThemeWrapper themeWrapper;
private PhotoViewer photoViewer;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -46,7 +62,14 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override @Override
public void onAttach(Activity activity){ public void onAttach(Activity activity){
super.onAttach(activity); super.onAttach(activity);
setTitle(R.string.edit_image); themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
ColorPalette.palettes.get(GlobalUserPreferences.color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
setTitle(R.string.add_alt_text);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState);
} }
@Override @Override
@@ -54,14 +77,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
View view=inflater.inflate(R.layout.fragment_image_description, container, false); View view=inflater.inflate(R.layout.fragment_image_description, container, false);
edit=view.findViewById(R.id.edit); edit=view.findViewById(R.id.edit);
ImageView image=view.findViewById(R.id.photo); image=view.findViewById(R.id.photo);
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
if(width>0 && height>0){
// image.setAspectRatio(Math.max(1f, (float)width/height));
}
image.setOnClickListener(v->openPhotoViewer());
Uri uri=getArguments().getParcelable("uri"); Uri uri=getArguments().getParcelable("uri");
Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
if(type==Attachment.Type.IMAGE)
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000)); ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
else
loadVideoThumbIntoView(image, uri);
edit.setText(getArguments().getString("existingDescription")); edit.setText(getArguments().getString("existingDescription"));
return view; return view;
} }
private void loadVideoThumbIntoView(ImageView target, Uri uri){
MastodonAPIController.runInBackground(()->{
Context context=getActivity();
if(context==null)
return;
try{
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
mmr.setDataSource(context, uri);
Bitmap frame=mmr.getFrameAtTime(3_000_000);
mmr.release();
int size=Math.max(frame.getWidth(), frame.getHeight());
int maxSize=V.dp(250);
if(size>maxSize){
float factor=maxSize/(float)size;
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
}
Bitmap finalFrame=frame;
target.post(()->target.setImageBitmap(finalFrame));
}catch(Exception x){
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
}
});
}
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
@@ -71,43 +128,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle}); inflater.inflate(R.menu.compose_image_description, menu);
int buttonStyle=ta.getResourceId(0, 0);
ta.recycle();
saveButton=new Button(getActivity(), null, 0, buttonStyle);
saveButton.setText(R.string.save);
saveButton.setOnClickListener(this::onSaveClick);
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(R.string.publish);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setPositiveButton(R.string.ok, null)
.show();
}
return true; return true;
} }
private void onSaveClick(View v){
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
.setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Attachment result){ public boolean onBackPressed(){
Bundle r=new Bundle(); deliverResult();
r.putParcelable("attachment", Parcels.wrap(result)); return false;
setResult(true, r);
Nav.finish(ComposeImageDescriptionFragment.this);
} }
@Override @Override
public void onError(ErrorResponse error){ protected LayoutInflater getToolbarLayoutInflater(){
error.showToast(getActivity()); return LayoutInflater.from(themeWrapper);
} }
})
.wrapProgress(getActivity(), R.string.saving, false) private void deliverResult(){
.exec(accountID); Bundle r=new Bundle();
r.putString("text", edit.getText().toString().trim());
r.putString("attachment", attachmentID);
setResult(true, r);
}
private void openPhotoViewer(){
Attachment fakeAttachment=new Attachment();
fakeAttachment.id="local";
fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
Uri uri=getArguments().getParcelable("uri");
fakeAttachment.url=uri.toString();
fakeAttachment.meta=new Attachment.Metadata();
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
int[] pos={0, 0};
image.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight());
image.setElevation(1f);
return true;
}
@Override
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
image.setTranslationX(translateX);
image.setTranslationY(translateY);
image.setScaleX(scale);
image.setScaleY(scale);
}
@Override
public void endPhotoViewTransition(){
Drawable d=image.getDrawable();
image.setImageDrawable(null);
image.setImageDrawable(d);
image.setTranslationX(0f);
image.setTranslationY(0f);
image.setScaleX(1f);
image.setScaleY(1f);
image.setElevation(0f);
}
@Nullable
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
return image.getDrawable();
}
@Override
public void photoViewerDismissed(){
photoViewer=null;
}
@Override
public void onRequestPermissions(String[] permissions){
}
});
photoViewer.removeMenu();
} }
} }

View File

@@ -17,7 +17,6 @@ import android.view.MotionEvent;
import android.view.SubMenu; import android.view.SubMenu;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@@ -37,10 +36,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hootsuite.nachos.NachoTextView; import com.hootsuite.nachos.NachoTextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists; import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags; import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CustomLocalTimeline; import org.joinmastodon.android.model.CustomLocalTimeline;
@@ -58,6 +58,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -66,7 +67,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition> implements ScrollableToTop { public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
private String accountID; private String accountID;
private TimelinesAdapter adapter; private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper; private final ItemTouchHelper itemTouchHelper;
@@ -129,7 +130,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
itemTouchHelper.attachToRecyclerView(list); itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false); refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
} }
@Override @Override
@@ -222,7 +223,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu); makeBackItem(listsMenu);
makeBackItem(hashtagsMenu); makeBackItem(hashtagsMenu);
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu)); TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu)); listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular); addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu)); hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@@ -235,9 +236,11 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
} }
private void saveTimelines() { private void saveTimelines() {
updated = true; updated=true;
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE)); AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
GlobalUserPreferences.save(); if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
prefs.timelines=data;
prefs.save();
} }
private void removeTimeline(int position) { private void removeTimeline(int position) {
@@ -249,7 +252,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false); onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
updateOptionsMenu(); updateOptionsMenu();
} }
@@ -271,21 +274,16 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) { private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
if (tags == null || tags.isEmpty()) return false; if (tags == null || tags.isEmpty()) return false;
editText.setText(tags); editText.setText(String.join(",", tags));
editText.chipifyAllUnterminatedTokens(); editText.chipifyAllUnterminatedTokens();
return true; return true;
} }
private NachoTextView prepareChipTextView(NachoTextView nacho) { private NachoTextView prepareChipTextView(NachoTextView nacho) {
//Ill Be Back nacho.addChipTerminator(',', BEHAVIOR_CHIPIFY_ALL);
nacho.setChipTerminators( nacho.addChipTerminator('\n', BEHAVIOR_CHIPIFY_ALL);
Map.of( nacho.addChipTerminator(' ', BEHAVIOR_CHIPIFY_ALL);
',', BEHAVIOR_CHIPIFY_ALL, nacho.addChipTerminator(';', BEHAVIOR_CHIPIFY_ALL);
'\n', BEHAVIOR_CHIPIFY_ALL,
' ', BEHAVIOR_CHIPIFY_ALL,
';', BEHAVIOR_CHIPIFY_ALL
)
);
nacho.enableEditChipOnTouch(true, true); nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens()); nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
return nacho; return nacho;
@@ -296,6 +294,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
Context ctx = getContext(); Context ctx = getContext();
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false); View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
View divider = view.findViewById(R.id.divider);
Button advancedBtn = view.findViewById(R.id.advanced); Button advancedBtn = view.findViewById(R.id.advanced);
EditText editText = view.findViewById(R.id.input); EditText editText = view.findViewById(R.id.input);
if (item != null) editText.setText(item.getCustomTitle()); if (item != null) editText.setText(item.getCustomTitle());
@@ -304,11 +303,12 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap); LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG; boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE); advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
view.findViewById(R.id.divider).setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
advancedBtn.setOnClickListener(l -> { advancedBtn.setOnClickListener(l -> {
advancedBtn.setSelected(!advancedBtn.isSelected()); advancedBtn.setSelected(!advancedBtn.isSelected());
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show); advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
UiUtils.beginLayoutTransition((ViewGroup) view);
}); });
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch); Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
@@ -321,7 +321,8 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none)); NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
if (item != null) { if (item != null) {
tagMain.setText(item.getHashtagName()); tagMain.setText(item.getHashtagName());
boolean hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()); boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced; hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced; hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
if (item.isHashtagLocalOnly()) { if (item.isHashtagLocalOnly()) {
@@ -332,6 +333,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
advancedBtn.setSelected(true); advancedBtn.setSelected(true);
advancedBtn.setText(R.string.sk_advanced_options_hide); advancedBtn.setText(R.string.sk_advanced_options_hide);
tagWrap.setVisibility(View.VISIBLE); tagWrap.setVisibility(View.VISIBLE);
divider.setVisibility(View.VISIBLE);
} }
} }

View File

@@ -5,7 +5,7 @@ import android.net.Uri;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses; import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -39,8 +39,8 @@ public class FavoritedStatusListFragment extends StatusListFragment{
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return FilterContext.ACCOUNT;
} }
@Override @Override

View File

@@ -0,0 +1,64 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
private Account account;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
setTitle(R.string.hashtags);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
}
@Override
protected void addAccountToKnown(Hashtag s){
}
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
// no-op
}
@Override
public Uri getWebUri(Uri.Builder base){
return null; // TODO
}
}

View File

@@ -48,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;
@@ -254,7 +254,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount))); followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount))); followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount))); postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE); followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE); followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
@@ -278,7 +278,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
actionWrap.setVisibility(View.VISIBLE); actionWrap.setVisibility(View.VISIBLE);
acceptWrap.setVisibility(View.GONE); acceptWrap.setVisibility(View.GONE);
rejectWrap.setVisibility(View.GONE); rejectWrap.setVisibility(View.GONE);
UiUtils.setRelationshipToActionButton(relationship, actionButton); UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
} }
} }
@@ -313,6 +313,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
private void onFollowRequestButtonClick(View v) { private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true); itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> { UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
if(getContext()==null) return;
itemView.setHasTransientState(false); itemView.setHasTransientState(false);
relationships.put(item.account.id, rel); relationships.put(item.account.id, rel);
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter(); RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
@@ -328,6 +329,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
private void onActionButtonClick(View v){ private void onActionButtonClick(View v){
itemView.setHasTransientState(true); itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{ UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
if(getContext()==null) return;
itemView.setHasTransientState(false); itemView.setHasTransientState(false);
relationships.put(item.account.id, rel); relationships.put(item.account.id, rel);
rebind(); rebind();

View File

@@ -21,7 +21,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID; private String nextMaxID;
private String accountID; private String accountID;
@@ -47,7 +47,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
} }
@Override @Override

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
@@ -24,4 +25,8 @@ public interface HasAccountID {
default Optional<Instance> getInstance() { default Optional<Instance> getInstance() {
return getSession().getInstance(); return getSession().getInstance();
} }
default AccountLocalPreferences getLocalPrefs() {
return AccountSessionManager.get(getAccountID()).getLocalPreferences();
}
} }

View File

@@ -0,0 +1,7 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
public interface HasElevationOnScrollListener {
ElevationOnScrollListener getElevationOnScrollListener();
}

View File

@@ -18,7 +18,7 @@ import org.joinmastodon.android.api.requests.tags.GetHashtag;
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed; import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline; import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.events.HashtagUpdatedEvent; import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.model.TimelineDefinition;
@@ -134,7 +134,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly) currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
@@ -168,12 +168,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override @Override
protected void onSetFabBottomInset(int inset){ protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset; ((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return FilterContext.PUBLIC;
} }
@Override @Override

View File

@@ -1,8 +1,10 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment; import android.app.Fragment;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.graphics.drawable.RippleDrawable;
import android.content.Intent; import android.content.Intent;
import android.graphics.Outline; import android.graphics.Outline;
import android.os.Build; import android.os.Build;
@@ -11,45 +13,41 @@ import android.service.notification.StatusBarNotification;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
import org.joinmastodon.android.DomainManager; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.E;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent; import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.events.NotificationReceivedEvent; import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverAccountsFragment;
import org.joinmastodon.android.fragments.discover.DiscoverFragment; import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment; import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@@ -67,42 +65,39 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private FragmentRootLinearLayout content; private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment; private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment; private NotificationsFragment notificationsFragment;
private DiscoverFragment searchFragment; private DiscoverFragment discoverFragment;
private ProfileFragment profileFragment; private ProfileFragment profileFragment;
private TabBar tabBar; private TabBar tabBar;
private View tabBarWrap; private View tabBarWrap;
private ImageView tabBarAvatar; private ImageView tabBarAvatar;
private ImageView notificationTabIcon;
@IdRes @IdRes
private int currentTab=R.id.tab_home; private int currentTab=R.id.tab_home;
private TextView notificationsBadge;
private String accountID; private String accountID;
private boolean isPleroma; private boolean isAkkoma;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
E.register(this);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
setTitle(R.string.mo_app_name); setTitle(R.string.mo_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isAkkoma) isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
.orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true); setRetainInstance(true);
// TODO: clean up
if(savedInstanceState==null){ if(savedInstanceState==null){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
homeTabFragment=new HomeTabFragment(); homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args); homeTabFragment.setArguments(args);
args=new Bundle(args); args=new Bundle(args);
args.putBoolean("disableDiscover", isPleroma); args.putBoolean("disableDiscover", isAkkoma);
args.putBoolean("noAutoLoad", true); args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment(); discoverFragment=new DiscoverFragment();
searchFragment.setArguments(args); discoverFragment.setArguments(args);
notificationsFragment=new NotificationsFragment(); notificationsFragment=new NotificationsFragment();
notificationsFragment.setArguments(args); notificationsFragment.setArguments(args);
args=new Bundle(args); args=new Bundle(args);
@@ -112,6 +107,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
profileFragment.setArguments(args); profileFragment.setArguments(args);
} }
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
} }
@Nullable @Nullable
@@ -129,24 +131,47 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
tabBar.setListeners(this::onTabSelected, this::onTabLongClick); tabBar.setListeners(this::onTabSelected, this::onTabLongClick);
tabBarWrap=content.findViewById(R.id.tabbar_wrap); tabBarWrap=content.findViewById(R.id.tabbar_wrap);
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava); // this one's for the pill haters (https://m3.material.io/components/navigation-bar/overview)
tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){ if(GlobalUserPreferences.disableM3PillActiveIndicator){
@Override tabBar.findViewById(R.id.tab_home_pill).setBackground(null);
public void getOutline(View view, Outline outline){ tabBar.findViewById(R.id.tab_search_pill).setBackground(null);
outline.setOval(0, 0, view.getWidth(), view.getHeight()); tabBar.findViewById(R.id.tab_notifications_pill).setBackground(null);
tabBar.findViewById(R.id.tab_profile_pill).setBackgroundResource(R.drawable.bg_tab_profile);
View[] tabs={
tabBar.findViewById(R.id.tab_home),
tabBar.findViewById(R.id.tab_search),
tabBar.findViewById(R.id.tab_notifications),
tabBar.findViewById(R.id.tab_profile)
};
for(View tab : tabs){
tab.setBackgroundResource(R.drawable.bg_tabbar_tab_ripple);
((RippleDrawable) tab.getBackground())
.setRadius(V.dp(GlobalUserPreferences.showNavigationLabels ? 56 : 42));
} }
}); }
if(!GlobalUserPreferences.showNavigationLabels){
tabBar.findViewById(R.id.tab_home_label).setVisibility(View.GONE);
tabBar.findViewById(R.id.tab_search_label).setVisibility(View.GONE);
tabBar.findViewById(R.id.tab_notifications_label).setVisibility(View.GONE);
tabBar.findViewById(R.id.tab_profile_label).setVisibility(View.GONE);
}
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL);
tabBarAvatar.setClipToOutline(true); tabBarAvatar.setClipToOutline(true);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self; Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28))); ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
notificationTabIcon=content.findViewById(R.id.tab_notifications); notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
updateNotificationBadge(); notificationsBadge.setVisibility(View.GONE);
if(savedInstanceState==null){ if(savedInstanceState==null){
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment) .add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment) .add(me.grishka.appkit.R.id.fragment_wrap, discoverFragment).hide(discoverFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment) .add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment) .add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit(); .commit();
@@ -173,7 +198,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onViewStateRestored(savedInstanceState); super.onViewStateRestored(savedInstanceState);
if(savedInstanceState==null) return; if(savedInstanceState==null) return;
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment"); homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment"); discoverFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment"); profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab"); currentTab=savedInstanceState.getInt("selectedTab");
@@ -181,7 +206,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
Fragment current=fragmentForTab(currentTab); Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.hide(homeTabFragment) .hide(homeTabFragment)
.hide(searchFragment) .hide(discoverFragment)
.hide(notificationsFragment) .hide(notificationsFragment)
.hide(profileFragment) .hide(profileFragment)
.show(current) .show(current)
@@ -197,7 +222,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override @Override
public boolean wantsLightStatusBar(){ public boolean wantsLightStatusBar(){
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme(); return !UiUtils.isDarkTheme();
} }
@Override @Override
@@ -209,14 +234,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){ if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom(); int inset=insets.getSystemWindowInsetBottom();
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0); tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0)); super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
}else{ }else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
} }
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0); WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
homeTabFragment.onApplyWindowInsets(topOnlyInsets); homeTabFragment.onApplyWindowInsets(topOnlyInsets);
searchFragment.onApplyWindowInsets(topOnlyInsets); discoverFragment.onApplyWindowInsets(topOnlyInsets);
notificationsFragment.onApplyWindowInsets(topOnlyInsets); notificationsFragment.onApplyWindowInsets(topOnlyInsets);
profileFragment.onApplyWindowInsets(topOnlyInsets); profileFragment.onApplyWindowInsets(topOnlyInsets);
} }
@@ -225,7 +250,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(tab==R.id.tab_home){ if(tab==R.id.tab_home){
return homeTabFragment; return homeTabFragment;
}else if(tab==R.id.tab_search){ }else if(tab==R.id.tab_search){
return searchFragment; return discoverFragment;
}else if(tab==R.id.tab_notifications){ }else if(tab==R.id.tab_notifications){
return notificationsFragment; return notificationsFragment;
}else if(tab==R.id.tab_profile){ }else if(tab==R.id.tab_profile){
@@ -255,7 +280,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab(); if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
currentTab=tab; currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
} }
private void maybeTriggerLoading(Fragment newFragment){ private void maybeTriggerLoading(Fragment newFragment){
@@ -266,7 +290,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
((DiscoverFragment) newFragment).loadData(); ((DiscoverFragment) newFragment).loadData();
}else if(newFragment instanceof NotificationsFragment){ }else if(newFragment instanceof NotificationsFragment){
((NotificationsFragment) newFragment).loadData(); ((NotificationsFragment) newFragment).loadData();
// TODO make an interface?
NotificationManager nm=getActivity().getSystemService(NotificationManager.class); NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
for (StatusBarNotification notification : nm.getActiveNotifications()) { for (StatusBarNotification notification : nm.getActiveNotifications()) {
if (accountID.equals(notification.getTag())) { if (accountID.equals(notification.getTag())) {
@@ -288,7 +311,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(tab==R.id.tab_search){ if(tab==R.id.tab_search){
onTabSelected(R.id.tab_search); onTabSelected(R.id.tab_search);
tabBar.selectTab(R.id.tab_search); tabBar.selectTab(R.id.tab_search);
searchFragment.selectSearch(); searchFragment.openSearch();
return true; return true;
} }
if(tab==R.id.tab_home){ if(tab==R.id.tab_home){
@@ -304,7 +327,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(currentTab==R.id.tab_profile) if(currentTab==R.id.tab_profile)
if (profileFragment.onBackPressed()) return true; if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search) if(currentTab==R.id.tab_search)
if (searchFragment.onBackPressed()) return true; if (discoverFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) { if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home); tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home); onTabSelected(R.id.tab_home);
@@ -319,52 +342,79 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putInt("selectedTab", currentTab); outState.putInt("selectedTab", currentTab);
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment); if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment); if (discoverFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", discoverFragment);
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment); if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
} }
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override @Override
public void onSuccess(List<Notification> notifications) { protected void onShown(){
if (notifications.size() > 0) { super.onShown();
try { reloadNotificationsForUnreadCount();
long newestId = Long.parseLong(notifications.get(0).id);
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
setNotificationBadge(newestId > lastSeenId);
} catch (Exception ignored) {
setNotificationBadge(false);
} }
public void reloadNotificationsForUnreadCount(){
List<Notification>[] notifications=new List[]{null};
String[] marker={null};
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
marker[0]=m;
if(notifications[0]!=null){
updateUnreadCount(notifications[0], marker[0]);
} }
});
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, false, true, new Callback<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
notifications[0]=result.items;
if(marker[0]!=null)
updateUnreadCount(notifications[0], marker[0]);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error){}
setNotificationBadge(false); });
}
}).exec(accountID);
} }
public void setNotificationBadge(boolean badge) { @SuppressLint("DefaultLocale")
notificationTabIcon.setImageResource(badge private void updateUnreadCount(List<Notification> notifications, String marker){
? R.drawable.ic_fluent_alert_28_selector_badged if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
: R.drawable.ic_fluent_alert_28_selector); V.setVisibilityAnimated(notificationsBadge, View.GONE);
}else{
V.setVisibilityAnimated(notificationsBadge, View.VISIBLE);
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
notificationsBadge.setText(String.format("%d+", notifications.size()));
}else{
int count=0;
for(Notification n:notifications){
if(n.id.equals(marker))
break;
count++;
}
notificationsBadge.setText(String.format("%d", count));
}
}
} }
@Subscribe @Subscribe
public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) { public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
if (notificationReceivedEvent.account.equals(accountID)) setNotificationBadge(true); if(!ev.accountID.equals(accountID))
return;
if(ev.clearUnread)
V.setVisibilityAnimated(notificationsBadge, View.GONE);
} }
@Subscribe @Subscribe
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) { public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
setNotificationBadge(false); if(!ev.accountID.equals(accountID))
return;
if(homeTabFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
&& lf instanceof BaseStatusListFragment<?> homeTimelineFragment)
homeTimelineFragment.rebuildAllDisplayItems();
if(notificationsFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
&& lf instanceof BaseStatusListFragment<?> l)
l.rebuildAllDisplayItems();
} }
@Override @Override

View File

@@ -12,6 +12,7 @@ import android.app.Fragment;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -37,13 +38,13 @@ import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements; import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.lists.GetLists; import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags; import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.HashtagUpdatedEvent; import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.events.ListDeletedEvent; import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent; import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
@@ -57,9 +58,11 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -74,8 +77,9 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent { public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent, HasElevationOnScrollListener {
private static final int ANNOUNCEMENTS_RESULT = 654; private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID; private String accountID;
@@ -93,7 +97,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private PopupMenu switcherPopup; private PopupMenu switcherPopup;
private final Map<Integer, ListTimeline> listItems = new HashMap<>(); private final Map<Integer, ListTimeline> listItems = new HashMap<>();
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>(); private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
private List<TimelineDefinition> timelineDefinitions; private List<TimelineDefinition> timelinesList;
private int count; private int count;
private Fragment[] fragments; private Fragment[] fragments;
private FrameLayout[] tabViews; private FrameLayout[] tabViews;
@@ -104,6 +108,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private View overflowActionView = null; private View overflowActionView = null;
private boolean announcementsBadged, settingsBadged; private boolean announcementsBadged, settingsBadged;
private ImageButton fab; private ImageButton fab;
private ElevationOnScrollListener elevationOnScrollListener;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -131,7 +136,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override @Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
FragmentRootLinearLayout rootView = new FragmentRootLinearLayout(getContext());
rootView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
FrameLayout view = new FrameLayout(getContext()); FrameLayout view = new FrameLayout(getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
rootView.addView(view);
inflater.inflate(R.layout.compose_fab, view); inflater.inflate(R.layout.compose_fab, view);
fab = view.findViewById(R.id.fab); fab = view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick); fab.setOnClickListener(this::onFabClick);
@@ -146,8 +155,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
args.putBoolean("__disable_fab", true); args.putBoolean("__disable_fab", true);
args.putBoolean("onlyPosts", true); args.putBoolean("onlyPosts", true);
for (int i = 0; i < timelineDefinitions.size(); i++) { for (int i=0; i < timelinesList.size(); i++) {
TimelineDefinition tl = timelineDefinitions.get(i); TimelineDefinition tl = timelinesList.get(i);
fragments[i] = tl.getFragment(); fragments[i] = tl.getFragment();
timelines[i] = tl; timelines[i] = tl;
} }
@@ -174,7 +183,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
overflowActionView.setOnClickListener(l -> overflowPopup.show()); overflowActionView.setOnClickListener(l -> overflowPopup.show());
overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener()); overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener());
return view; return rootView;
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@@ -249,6 +258,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}); });
} }
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
if(GithubSelfUpdater.needSelfUpdating()){ if(GithubSelfUpdater.needSelfUpdating()){
updateUpdateState(GithubSelfUpdater.getInstance().getState()); updateUpdateState(GithubSelfUpdater.getInstance().getState());
} }
@@ -295,6 +306,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).exec(accountID); }).exec(accountID);
} }
public ElevationOnScrollListener getElevationOnScrollListener() {
return elevationOnScrollListener;
}
private void onFabClick(View v){ private void onFabClick(View v){
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) { if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
l.onFabClick(v); l.onFabClick(v);
@@ -326,8 +341,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
hashtagsMenu.clear(); hashtagsMenu.clear();
hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0); hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu)); UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu));
hashtagsItems.forEach((id, hashtag) -> { hashtagsItems.entrySet().stream()
MenuItem item = hashtagsMenu.add(Menu.NONE, id, Menu.NONE, hashtag.name); .sorted(Comparator.comparing(x -> x.getValue().name, String.CASE_INSENSITIVE_ORDER))
.forEach(entry -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular); item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item); UiUtils.insetPopupMenuIcon(ctx, item);
}); });
@@ -472,10 +489,19 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
&& fabulous.isScrolling(); && fabulous.isScrolling();
} }
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (elevationOnScrollListener != null) elevationOnScrollListener.setViews(getToolbar());
}
private void updateSwitcherIcon(int i) { private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes); timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext())); timelineTitle.setText(timelines[i].getTitle(getContext()));
showFab(); showFab();
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f) {
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
}
} }
@Override @Override
@@ -645,8 +671,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override @Override
protected void onShown() { protected void onShown() {
super.onShown(); super.onShown();
Object pinnedTimelines = GlobalUserPreferences.pinnedTimelines.get(accountID); Object timelines = AccountSessionManager.get(accountID).getLocalPreferences().timelines;
if (pinnedTimelines != null && timelineDefinitions != pinnedTimelines) UiUtils.restartApp(); if (timelines != null && timelinesList!= timelines) UiUtils.restartApp();
} }
@Override @Override
@@ -710,6 +736,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return hashtagsItems.values(); return hashtagsItems.values();
} }
public Fragment getCurrentFragment() {
return fragments[pager.getCurrentItem()];
}
public ImageButton getFab() { public ImageButton getFab() {
return fab; return fab;
} }

View File

@@ -11,11 +11,13 @@ import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.requests.markers.SaveMarkers; import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate; import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -49,8 +51,9 @@ public class HomeTimelineFragment extends StatusListFragment {
} }
private boolean typeFilterPredicate(Status s) { private boolean typeFilterPredicate(Status s) {
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) && AccountLocalPreferences lp=getLocalPrefs();
(GlobalUserPreferences.showBoosts || s.reblog == null); return (lp.showReplies || s.inReplyToId == null) &&
(lp.showBoosts || s.reblog == null);
} }
private List<Status> filterPosts(List<Status> items) { private List<Status> filterPosts(List<Status> items) {
@@ -110,7 +113,7 @@ public class HomeTimelineFragment extends StatusListFragment {
new SaveMarkers(topPostID, null) new SaveMarkers(topPostID, null)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(SaveMarkers.Response result){ public void onSuccess(TimelineMarkers result){
} }
@Override @Override
@@ -123,8 +126,8 @@ public class HomeTimelineFragment extends StatusListFragment {
} }
} }
public void onStatusCreated(StatusCreatedEvent ev){ public void onStatusCreated(Status status){
prependItems(Collections.singletonList(ev.status), true); prependItems(Collections.singletonList(status), true);
} }
private void loadNewPosts(){ private void loadNewPosts(){
@@ -134,7 +137,7 @@ public class HomeTimelineFragment extends StatusListFragment {
// we'll get the currently topmost post as last in the response. This way we know there's no gap // we'll get the currently topmost post as last in the response. This way we know there's no gap
// between the existing and newly loaded parts of the timeline. // between the existing and newly loaded parts of the timeline.
String sinceID=data.size()>1 ? data.get(1).id : "1"; String sinceID=data.size()>1 ? data.get(1).id : "1";
currentRequest=new GetHomeTimeline(null, null, 20, sinceID) currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
@@ -151,8 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true; result.get(result.size()-1).hasGapAfter=true;
toAdd=result; toAdd=result;
} }
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext()); AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){ if(!toAdd.isEmpty()){
prependItems(toAdd, true); prependItems(toAdd, true);
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton(); if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
@@ -169,7 +171,7 @@ public class HomeTimelineFragment extends StatusListFragment {
.exec(accountID); .exec(accountID);
if (parent.getParentFragment() instanceof HomeFragment homeFragment) { if (parent.getParentFragment() instanceof HomeFragment homeFragment) {
homeFragment.updateNotificationBadge(); homeFragment.reloadNotificationsForUnreadCount();
} }
} }
@@ -182,7 +184,7 @@ public class HomeTimelineFragment extends StatusListFragment {
V.setVisibilityAnimated(item.text, View.GONE); V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem(); GapStatusDisplayItem gap=item.getItem();
dataLoading=true; dataLoading=true;
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null) currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
@@ -239,6 +241,7 @@ public class HomeTimelineFragment extends StatusListFragment {
insertedPosts.add(s); insertedPosts.add(s);
} }
} }
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, getFilterContext());
if(targetList.isEmpty()){ if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none. // oops. We didn't add new posts, but at least we know there are none.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos); adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
@@ -285,8 +288,8 @@ public class HomeTimelineFragment extends StatusListFragment {
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.HOME; return FilterContext.HOME;
} }
@Override @Override

View File

@@ -18,7 +18,7 @@ import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline; import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.events.ListDeletedEvent; import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent; import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.model.TimelineDefinition;
@@ -134,7 +134,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override @Override
protected void doLoadData(int offset, int count) { protected void doLoadData(int offset, int count) {
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null) currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this) { .setCallback(new SimpleCallback<>(this) {
@Override @Override
public void onSuccess(List<Status> result) { public void onSuccess(List<Status> result) {
@@ -167,8 +167,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.HOME; return FilterContext.HOME;
} }
@Override @Override

View File

@@ -42,7 +42,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private String profileAccountId; private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>(); private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
@@ -80,7 +80,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
} }
@Override @Override

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.View;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import androidx.annotation.CallSuper;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T>{
protected ElevationOnScrollListener elevationOnScrollListener;
public MastodonRecyclerFragment(int perPage){
super(perPage);
}
public MastodonRecyclerFragment(int layout, int perPage){
super(layout, perPage);
}
protected List<View> getViewsForElevationEffect(){
Toolbar toolbar=getToolbar();
return toolbar!=null ? Collections.singletonList(toolbar) : Collections.emptyList();
}
@Override
@CallSuper
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if (getParentFragment() instanceof HasElevationOnScrollListener elevator)
list.addOnScrollListener(elevator.getElevationOnScrollListener());
else if(wantsElevationOnScrollEffect())
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
if(refreshLayout!=null)
setRefreshLayoutColors(refreshLayout);
}
@Override
@CallSuper
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(elevationOnScrollListener!=null){
elevationOnScrollListener.setViews(getViewsForElevationEffect());
}
}
protected boolean wantsElevationOnScrollEffect(){
return true;
}
public List<T> getData() {
return data;
}
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
List<Integer> colors = new ArrayList<>(Arrays.asList(
UiUtils.isDarkTheme() ? R.color.primary_200 : R.color.primary_600,
UiUtils.isDarkTheme() ? R.color.red_primary_200 : R.color.red_primary_600,
UiUtils.isDarkTheme() ? R.color.green_primary_200 : R.color.green_primary_600,
UiUtils.isDarkTheme() ? R.color.blue_primary_200 : R.color.blue_primary_600,
UiUtils.isDarkTheme() ? R.color.purple_200 : R.color.purple_600
));
int primary = UiUtils.getThemeColorRes(l.getContext(),
UiUtils.isDarkTheme() ? R.attr.colorPrimary200 : R.attr.colorPrimary600);
if (!colors.contains(primary)) colors.add(0, primary);
int offset = colors.indexOf(primary);
int[] sorted = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
sorted[i] = colors.get((i + offset) % colors.size());
}
l.setColorSchemeResources(sorted);
int colorBackground=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Primary);
l.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
}
}

View File

@@ -11,6 +11,15 @@ import androidx.annotation.CallSuper;
import me.grishka.appkit.fragments.ToolbarFragment; import me.grishka.appkit.fragments.ToolbarFragment;
public abstract class MastodonToolbarFragment extends ToolbarFragment{ public abstract class MastodonToolbarFragment extends ToolbarFragment{
public MastodonToolbarFragment(){
super();
}
protected MastodonToolbarFragment(int layout){
super(layout);
}
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -24,6 +25,9 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests; import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FollowRequestHandledEvent; import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
@@ -31,6 +35,8 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@@ -38,15 +44,19 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent { public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener {
private TabLayout tabLayout; TabLayout tabLayout;
private ViewPager2 pager; private ViewPager2 pager;
private FrameLayout[] tabViews; private FrameLayout[] tabViews;
private View tabsDivider;
private TabLayoutMediator tabLayoutMediator; private TabLayoutMediator tabLayoutMediator;
String unreadMarker, realUnreadMarker;
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment; private MenuItem markAllReadItem;
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private ElevationOnScrollListener elevationOnScrollListener;
private String accountID; private String accountID;
@Override @Override
@@ -72,11 +82,19 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
setTitle(R.string.notifications); setTitle(R.string.notifications);
} }
@Override
public void onShown() {
super.onShown();
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu); inflater.inflate(R.menu.notifications, menu);
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications); menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests); markAllReadItem=menu.findItem(R.id.mark_all_read);
updateMarkAllReadButton();
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read);
} }
@Override @Override
@@ -93,25 +111,49 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
} }
}); });
return true; return true;
} else if (item.getItemId() == R.id.mark_all_read) {
markAsRead();
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
nlf.resetUnreadBackground();
}
return true;
} }
return false; return false;
} }
void markAsRead(){
if(allNotificationsFragment.getData().isEmpty()) return;
String id=allNotificationsFragment.getData().get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
if (allNotificationsFragment.isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(id).exec(accountID);
}
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
updateMarkAllReadButton();
}
}
public void updateMarkAllReadButton(){
markAllReadItem.setVisible(!allNotificationsFragment.getData().isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(allNotificationsFragment.getData().get(0).id));
}
@Override @Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false); LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
tabLayout=view.findViewById(R.id.tabbar); tabLayout=view.findViewById(R.id.tabbar);
tabsDivider=view.findViewById(R.id.tabs_divider);
pager=view.findViewById(R.id.pager); pager=view.findViewById(R.id.pager);
UiUtils.reduceSwipeSensitivity(pager); UiUtils.reduceSwipeSensitivity(pager);
tabViews=new FrameLayout[3]; tabViews=new FrameLayout[2];
for(int i=0;i<tabViews.length;i++){ for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity()); FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){ tabView.setId(switch(i){
case 0 -> R.id.notifications_all; case 0 -> R.id.notifications_all;
case 1 -> R.id.notifications_mentions; case 1 -> R.id.notifications_mentions;
case 2 -> R.id.notifications_posts;
default -> throw new IllegalStateException("Unexpected value: "+i); default -> throw new IllegalStateException("Unexpected value: "+i);
}); });
tabView.setVisibility(View.GONE); tabView.setVisibility(View.GONE);
@@ -120,7 +162,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
} }
tabLayout.setTabTextSize(V.dp(16)); tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override @Override
public void onTabSelected(TabLayout.Tab tab) {} public void onTabSelected(TabLayout.Tab tab) {}
@@ -140,6 +182,8 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override @Override
public void onPageSelected(int position){ public void onPageSelected(int position){
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
if(position==0) if(position==0)
return; return;
Fragment _page=getFragmentForPage(position); Fragment _page=getFragmentForPage(position);
@@ -163,15 +207,9 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
mentionsFragment=new NotificationsListFragment(); mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args); mentionsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyPosts", true);
postsFragment=new NotificationsListFragment();
postsFragment.setArguments(args);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.notifications_all, allNotificationsFragment) .add(R.id.notifications_all, allNotificationsFragment)
.add(R.id.notifications_mentions, mentionsFragment) .add(R.id.notifications_mentions, mentionsFragment)
.add(R.id.notifications_posts, postsFragment)
.commit(); .commit();
} }
@@ -181,10 +219,8 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tab.setText(switch(position){ tab.setText(switch(position){
case 0 -> R.string.all_notifications; case 0 -> R.string.all_notifications;
case 1 -> R.string.mentions; case 1 -> R.string.mentions;
case 2 -> R.string.posts;
default -> throw new IllegalStateException("Unexpected value: "+position); default -> throw new IllegalStateException("Unexpected value: "+position);
}); });
tab.view.textView.setAllCaps(true);
} }
}); });
tabLayoutMediator.attach(); tabLayoutMediator.attach();
@@ -192,6 +228,28 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
return view; return view;
} }
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar(), tabLayout);
elevationOnScrollListener.setDivider(tabsDivider);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (elevationOnScrollListener == null) return;
elevationOnScrollListener.setViews(getToolbar(), tabLayout);
if (getCurrentFragment() instanceof IsOnTop f) {
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
}
}
@Override
public ElevationOnScrollListener getElevationOnScrollListener() {
return elevationOnScrollListener;
}
public void refreshFollowRequestsBadge() { public void refreshFollowRequestsBadge() {
new GetFollowRequests(null, 1).setCallback(new Callback<>() { new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override @Override
@@ -212,11 +270,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override @Override
public void scrollToTop(){ public void scrollToTop(){
if (getFragmentForPage(pager.getCurrentItem()).isOnTop() && GlobalUserPreferences.doubleTapToSwipe) {
int nextPage = (pager.getCurrentItem() + 1) % tabViews.length;
pager.setCurrentItem(nextPage, true);
return;
}
getFragmentForPage(pager.getCurrentItem()).scrollToTop(); getFragmentForPage(pager.getCurrentItem()).scrollToTop();
} }
@@ -237,11 +290,14 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
return switch(page){ return switch(page){
case 0 -> allNotificationsFragment; case 0 -> allNotificationsFragment;
case 1 -> mentionsFragment; case 1 -> mentionsFragment;
case 2 -> postsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page); default -> throw new IllegalStateException("Unexpected value: "+page);
}; };
} }
public Fragment getCurrentFragment() {
return getFragmentForPage(pager.getCurrentItem());
}
@Override @Override
public void onProvideAssistContent(AssistContent assistContent) { public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent); callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
@@ -263,7 +319,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override @Override
public int getItemCount(){ public int getItemCount(){
return 3; return 2;
} }
@Override @Override

View File

@@ -1,6 +1,9 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@@ -9,51 +12,46 @@ import android.view.View;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{ public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
private boolean onlyMentions; private boolean onlyMentions;
private boolean onlyPosts; private boolean onlyPosts;
private String maxID; private String maxID;
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS); private boolean reloadingFromCache;
private DiscoverInfoBannerHelper bannerHelper;
@Override @Override
protected boolean wantsComposeButton() { protected boolean wantsComposeButton() {
@@ -64,6 +62,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
E.register(this); E.register(this);
if(savedInstanceState!=null){
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
}
if (onlyPosts) {
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
}
} }
@Override @Override
@@ -77,59 +82,39 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
super.onAttach(activity); super.onAttach(activity);
onlyMentions=getArguments().getBoolean("onlyMentions", false); onlyMentions=getArguments().getBoolean("onlyMentions", false);
onlyPosts=getArguments().getBoolean("onlyPosts", false); onlyPosts=getArguments().getBoolean("onlyPosts", false);
} setTitle(R.string.notifications);
@Override
public void onRefresh() {
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment notificationsFragment) {
notificationsFragment.refreshFollowRequestsBadge();
}
} }
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){ protected List<StatusDisplayItem> buildDisplayItems(Notification n){
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null : NotificationHeaderStatusDisplayItem titleItem;
n.report.targetAccount; if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
Emoji emoji = new Emoji(); titleItem=null;
if(n.emojiUrl!=null){ }else{
emoji.shortcode=n.emoji.substring(1,n.emoji.length()-1); titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
emoji.url=n.emojiUrl; }
emoji.staticUrl=n.emojiUrl; if (n.type == Notification.Type.FOLLOW_REQUEST) {
emoji.visibleInPicker=false; ArrayList<StatusDisplayItem> items = new ArrayList<>();
items.add(titleItem);
items.add(new AccountCardStatusDisplayItem(n.id, this, n.account, n));
return items;
} }
String extraText=switch(n.type){
case FOLLOW -> getString(R.string.user_followed_you);
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
case MENTION, STATUS -> null;
case REBLOG -> getString(R.string.notification_boosted);
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
case UPDATE -> getString(R.string.sk_post_edited);
case SIGN_UP -> getString(R.string.sk_signed_up);
case REPORT -> getString(R.string.sk_reported);
case REACTION, PLEROMA_EMOJI_REACTION ->
n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null;
if(n.status!=null){ if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS); int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); // | StatusDisplayItem.FLAG_NO_HEADER);
if (GlobalUserPreferences.spectatorMode)
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
if (!getLocalPrefs().showEmojiReactionsInLists)
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
if(titleItem!=null) if(titleItem!=null)
items.add(0, titleItem); items.add(0, titleItem);
return items; return items;
}else if(titleItem!=null){ }else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, return Collections.singletonList(titleItem);
reportTarget != null ? reportTarget : n.account, n);
TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ?
new TextStatusDisplayItem(n.id, n.report.comment, this,
Status.ofFake(n.id, n.report.comment, n.createdAt), true) :
null;
return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card);
}else{ }else{
return Collections.emptyList(); return Collections.emptyList();
} }
} }
@Override @Override
protected void addAccountToKnown(Notification s){ protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id)) if(!knownAccounts.containsKey(s.account.id))
@@ -144,52 +129,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance() AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController() .getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){ .getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@Override @Override
public void onSuccess(CacheablePaginatedResponse<List<Notification>> result){ public void onSuccess(PaginatedResponse<List<Notification>> result){
if (getActivity() == null) return; if(getActivity()==null)
if(refreshing) return;
relationships.clear();
maxID=result.maxID; maxID=result.maxID;
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty()); onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
Set<String> needRelationships=result.items.stream() reloadingFromCache=false;
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id)) if (getParentFragment() instanceof NotificationsFragment nf) {
.map(ntf->ntf.account.id) nf.updateMarkAllReadButton();
.collect(Collectors.toSet());
loadRelationships(needRelationships);
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
} }
} }
}); });
} }
@Override @Override
protected void onRelationshipsLoaded(){ protected void onShown(){
if(getActivity()==null) super.onShown();
return; if(!dataLoading){
for(int i=0;i<list.getChildCount();i++){ if(onlyMentions){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i)); refresh();
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder) }else{
accountHolder.rebind(); reloadingFromCache=true;
refresh();
}
} }
} }
@Override @Override
protected void onShown(){ protected void onHidden(){
super.onShown(); super.onHidden();
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) resetUnreadBackground();
// loadData();
} }
@Override @Override
@@ -205,7 +176,50 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this)); list.addItemDecoration(new InsetStatusItemDecoration(this));
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap); list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
private Rect tmpRect=new Rect();
{
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if (getParentFragment() instanceof NotificationsFragment nf) {
if(TextUtils.isEmpty(nf.unreadMarker))
return;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
String itemID=holder.getItemID();
if(ObjectIdComparator.INSTANCE.compare(itemID, nf.unreadMarker)>0){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
c.drawRect(tmpRect, paint);
}
}
}
}
}
}, 0);
}
@Override
protected List<View> getViewsForElevationEffect(){
if (getParentFragment() instanceof NotificationsFragment nf) {
ArrayList<View> views=new ArrayList<>(super.getViewsForElevationEffect());
views.add(nf.tabLayout);
return views;
} else {
return super.getViewsForElevationEffect();
}
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("onlyMentions", onlyMentions);
outState.putBoolean("onlyPosts", onlyPosts);
} }
private Notification getNotificationByID(String id){ private Notification getNotificationByID(String id){
@@ -238,12 +252,15 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if (n.status == null) continue; if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){ if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev); n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0;i<list.getChildCount();i++){ for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i)); RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){ if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind(); footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){ }else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind(); footer.rebind();
}else if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && ev.viewHolder!=holder){
reactions.rebind();
} }
} }
} }
@@ -252,6 +269,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if (n.status == null) continue; if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){ if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev); n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
} }
} }
} }
@@ -289,6 +307,40 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
adapter.notifyItemRangeRemoved(index, lastIndex-index); adapter.notifyItemRangeRemoved(index, lastIndex-index);
} }
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
void resetUnreadBackground(){
if (getParentFragment() instanceof NotificationsFragment nf) {
nf.unreadMarker=nf.realUnreadMarker;
list.invalidate();
}
}
@Override
public void onRefresh(){
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment nf) {
if (!onlyMentions && !onlyPosts) nf.markAsRead();
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
nf.unreadMarker=nf.realUnreadMarker=m;
nf.updateMarkAllReadButton();
});
}
resetUnreadBackground();
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
if (bannerHelper == null) return super.getAdapter();
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override @Override
public Uri getWebUri(Uri.Builder base) { public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() return base.path(isInstanceAkkoma()

View File

@@ -7,21 +7,21 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.model.TimelineDefinition;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public abstract class PinnableStatusListFragment extends StatusListFragment implements DomainDisplay { public abstract class PinnableStatusListFragment extends StatusListFragment {
protected boolean pinnedUpdated; protected List<TimelineDefinition> timelines;
protected List<TimelineDefinition> pinnedTimelines;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID))); timelines=new ArrayList<>(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
} }
@Override @Override
@@ -31,7 +31,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment impl
} }
protected boolean isPinned() { protected boolean isPinned() {
return pinnedTimelines.contains(makeTimelineDefinition()); return timelines.contains(makeTimelineDefinition());
} }
protected void updatePinButton(MenuItem pin) { protected void updatePinButton(MenuItem pin) {
@@ -54,29 +54,18 @@ public abstract class PinnableStatusListFragment extends StatusListFragment impl
} }
protected void togglePin(MenuItem pin) { protected void togglePin(MenuItem pin) {
pinnedUpdated = true; onPinnedUpdated(true);
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
TimelineDefinition def = makeTimelineDefinition(); TimelineDefinition def = makeTimelineDefinition();
boolean pinned = isPinned(); boolean pinned = isPinned();
if (pinned) pinnedTimelines.remove(def); if (pinned) timelines.remove(def);
else pinnedTimelines.add(def); else timelines.add(def);
Toast.makeText(getContext(), pinned ? R.string.sk_unpinned_timeline : R.string.sk_pinned_timeline, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), pinned ? R.string.sk_unpinned_timeline : R.string.sk_pinned_timeline, Toast.LENGTH_SHORT).show();
GlobalUserPreferences.pinnedTimelines.put(accountID, pinnedTimelines); AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
GlobalUserPreferences.save(); prefs.timelines=new ArrayList<>(timelines);
prefs.save();
updatePinButton(pin); updatePinButton(pin);
} }
protected Bundle getResultArgs() { public void onPinnedUpdated(boolean pinned) {}
return new Bundle();
}
@Override
public void onDestroy() {
super.onDestroy();
Bundle resultArgs = getResultArgs();
if (pinnedUpdated) {
resultArgs.putBoolean("pinnedUpdated", true);
setResult(true, resultArgs);
}
}
} }

View File

@@ -6,7 +6,7 @@ import android.os.Bundle;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -41,8 +41,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT; return FilterContext.ACCOUNT;
} }
@Override @Override

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
@@ -13,23 +12,12 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.ImageView;
import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetPrivateNote;
import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
@@ -39,8 +27,11 @@ import org.joinmastodon.android.ui.views.LinkedTextView;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import me.grishka.appkit.api.Callback; import androidx.annotation.NonNull;
import me.grishka.appkit.api.ErrorResponse; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.fragments.WindowInsetsAwareFragment; import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
@@ -53,21 +44,15 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{
private static final int MAX_FIELDS=4; static final int MAX_FIELDS=Integer.MAX_VALUE;
public UsableRecyclerView list; public UsableRecyclerView list;
public FrameLayout noteWrap;
public EditText noteEdit;
private String accountID;
private String profileAccountID;
private String note;
private List<AccountField> fields=Collections.emptyList(); private List<AccountField> fields=Collections.emptyList();
private AboutAdapter adapter; private AboutAdapter adapter;
private Paint dividerPaint=new Paint();
private boolean isInEditMode; private boolean isInEditMode;
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private RecyclerView.ViewHolder draggedViewHolder;
private ListImageLoaderWrapper imgLoader; private ListImageLoaderWrapper imgLoader;
private boolean editDirty;
public void setFields(List<AccountField> fields){ public void setFields(List<AccountField> fields){
this.fields=fields; this.fields=fields;
@@ -79,101 +64,37 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
public void setNote(String note, String accountID, String profileAccountID){
this.note=note;
this.accountID=accountID;
this.profileAccountID=profileAccountID;
// noteWrap.setVisibility(View.VISIBLE);
// noteEdit.setVisibility(View.VISIBLE);
// noteEdit.setText(note);
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view = inflater.inflate(R.layout.fragment_profile_about, null); list=new UsableRecyclerView(getActivity());
list.setId(R.id.list);
noteEdit = view.findViewById(R.id.note_edit);
noteWrap = view.findViewById(R.id.note_edit_wrap);
ImageButton noteEditConfirm = view.findViewById(R.id.note_edit_confirm);
noteEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
noteEditConfirm.setVisibility(View.VISIBLE);
noteEditConfirm.animate()
.alpha(1.0f)
.setDuration(700);
} else {
noteEditConfirm.animate()
.alpha(0.0f)
.setDuration(700);
noteEditConfirm.setVisibility(View.INVISIBLE);
}
});
noteEditConfirm.setOnClickListener((v -> {
if (!noteEdit.getText().toString().trim().equals(note)) {
savePrivateNote();
}
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
noteEdit.clearFocus();
}));
list = view.findViewById(R.id.list);
list.setItemAnimator(new BetterItemAnimator()); list.setItemAnimator(new BetterItemAnimator());
list.setDrawSelectorOnTop(true); list.setDrawSelectorOnTop(true);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null); imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new AboutAdapter()); list.setAdapter(adapter=new AboutAdapter());
int pad=V.dp(16); list.setPadding(0, V.dp(16), 0, 0);
list.setPadding(pad, pad, pad, pad);
list.setClipToPadding(false); list.setClipToPadding(false);
dividerPaint.setStyle(Paint.Style.STROKE); return list;
dividerPaint.setStrokeWidth(V.dp(1));
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
for(int i=0;i<parent.getChildCount();i++){
View item=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(item);
int draggedPos=draggedViewHolder==null ? -1 : draggedViewHolder.getAbsoluteAdapterPosition();
if(pos<adapter.getItemCount()-1 && pos!=draggedPos && pos!=draggedPos-1){
float y=item.getY()+item.getHeight();
dividerPaint.setAlpha(Math.round(255*item.getAlpha()));
c.drawLine(item.getLeft(), y, item.getRight(), y, dividerPaint);
} }
}
}
});
return view;
}
private void savePrivateNote(){
new SetPrivateNote(profileAccountID, noteEdit.getText().toString()).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result) {}
@Override
public void onError(ErrorResponse result) {
Toast.makeText(getActivity(), getString(R.string.mo_personal_note_update_failed), Toast.LENGTH_LONG).show();
}
}).exec(accountID);
}
public void enterEditMode(List<AccountField> editableFields){ public void enterEditMode(List<AccountField> editableFields){
isInEditMode=true; isInEditMode=true;
fields=editableFields; fields=editableFields;
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
dragHelper.attachToRecyclerView(list); dragHelper.attachToRecyclerView(list);
editDirty=false;
} }
public List<AccountField> getFields(){ public List<AccountField> getFields(){
return fields; return fields;
} }
public boolean isEditDirty(){
return editDirty;
}
@Override @Override
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
@@ -248,36 +169,25 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
} }
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{ private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
protected ShapeDrawable background=new ShapeDrawable();
public BaseViewHolder(int layout){ public BaseViewHolder(int layout){
super(getActivity(), layout, list); super(getActivity(), layout, list);
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
itemView.setBackground(background);
} }
@Override @Override
public void onBind(AccountField item){ public void onBind(AccountField item){
boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1;
float radius=V.dp(10);
float[] rad=new float[8];
if(first)
rad[0]=rad[1]=rad[2]=rad[3]=radius;
if(last)
rad[4]=rad[5]=rad[6]=rad[7]=radius;
background.setShape(new RoundRectShape(rad, null, null));
itemView.invalidateOutline();
} }
} }
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{ private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
private TextView title; private final TextView title;
private LinkedTextView value; private final LinkedTextView value;
// private final ImageView verifiedIcon;
public AboutViewHolder(){ public AboutViewHolder(){
super(R.layout.item_profile_about); super(R.layout.item_profile_about);
title=findViewById(R.id.title); title=findViewById(R.id.title);
value=findViewById(R.id.value); value=findViewById(R.id.value);
// verifiedIcon=findViewById(R.id.verified_icon);
} }
@Override @Override
@@ -285,20 +195,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
super.onBind(item); super.onBind(item);
title.setText(item.parsedName); title.setText(item.parsedName);
value.setText(item.parsedValue); value.setText(item.parsedValue);
if(item.verifiedAt!=null){ // verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
} }
@Override @Override
@@ -316,27 +213,38 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
} }
private class EditableAboutViewHolder extends BaseViewHolder{ private class EditableAboutViewHolder extends BaseViewHolder{
private EditText title; private final EditText title;
private EditText value; private final EditText value;
private boolean ignoreTextChange;
public EditableAboutViewHolder(){ public EditableAboutViewHolder(){
super(R.layout.item_profile_about_editable); super(R.layout.onboarding_profile_field);
title=findViewById(R.id.title); title=findViewById(R.id.title);
value=findViewById(R.id.value); value=findViewById(R.id.content);
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{ findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
dragHelper.startDrag(this); dragHelper.startDrag(this);
return true; return true;
}); });
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString())); title.addTextChangedListener(new SimpleTextWatcher(e->{
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString())); item.name=e.toString();
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick); if(!ignoreTextChange)
editDirty=true;
}));
value.addTextChangedListener(new SimpleTextWatcher(e->{
item.value=e.toString();
if(!ignoreTextChange)
editDirty=true;
}));
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
} }
@Override @Override
public void onBind(AccountField item){ public void onBind(AccountField item){
super.onBind(item); super.onBind(item);
ignoreTextChange=true;
title.setText(item.name); title.setText(item.name);
value.setText(item.value); value.setText(item.value);
ignoreTextChange=false;
} }
private void onRemoveRowClick(View v){ private void onRemoveRowClick(View v){
@@ -388,8 +296,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
} }
} }
adapter.notifyItemMoved(fromPosition, toPosition); adapter.notifyItemMoved(fromPosition, toPosition);
((BindableViewHolder)viewHolder).rebind(); ((BindableViewHolder<?>)viewHolder).rebind();
((BindableViewHolder)target).rebind(); ((BindableViewHolder<?>)target).rebind();
return true; return true;
} }
@@ -404,7 +312,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){ if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(me.grishka.appkit.R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw() viewHolder.itemView.setTag(me.grishka.appkit.R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder;
} }
} }
@@ -412,7 +319,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){ public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder); super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=null;
} }
@Override @Override

View File

@@ -1,50 +0,0 @@
package org.joinmastodon.android.fragments;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
public abstract class RecyclerFragment<T> extends BaseRecyclerFragment<T> {
public RecyclerFragment(int perPage) {
super(perPage);
}
public RecyclerFragment(int layout, int perPage) {
super(layout, perPage);
}
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (refreshLayout != null) setRefreshLayoutColors(refreshLayout);
}
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
List<Integer> colors = new ArrayList<>(Arrays.asList(
R.color.primary_600,
R.color.red_primary_600,
R.color.green_primary_600,
R.color.blue_primary_600,
R.color.purple_600
));
int primary = UiUtils.getThemeColorRes(l.getContext(), R.attr.colorPrimary600);
if (!colors.contains(primary)) colors.add(0, primary);
int offset = colors.indexOf(primary);
int[] sorted = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
sorted[i] = colors.get((i + offset) % colors.size());
}
l.setColorSchemeResources(sorted);
}
}

View File

@@ -81,7 +81,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) { protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null); return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, false, true, null);
} }
@Override @Override

View File

@@ -7,20 +7,27 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.Button; import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment; import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment; import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -37,11 +44,16 @@ public class SplashFragment extends AppKitFragment{
private View artContainer, blueFill, greenFill; private View artContainer, blueFill, greenFill;
private InterpolatingMotionEffect motionEffect; private InterpolatingMotionEffect motionEffect;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill; private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
private ProgressBarButton defaultServerButton;
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context); motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
loadAndChooseDefaultServer();
} }
@Nullable @Nullable
@@ -50,9 +62,14 @@ public class SplashFragment extends AppKitFragment{
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false); contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick); contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick); contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
Button joinDefault=contentView.findViewById(R.id.btn_join_default_server); defaultServerButton=contentView.findViewById(R.id.btn_join_default_server);
joinDefault.setText(getString(R.string.join_default_server, DEFAULT_SERVER)); defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
joinDefault.setOnClickListener(this::onJoinDefaultServerClick); defaultServerButton.setOnClickListener(this::onJoinDefaultServerClick);
defaultServerProgress=contentView.findViewById(R.id.action_progress);
if(loadingDefaultServer){
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick); contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick);
artClouds=contentView.findViewById(R.id.art_clouds); artClouds=contentView.findViewById(R.id.art_clouds);
@@ -96,12 +113,22 @@ public class SplashFragment extends AppKitFragment{
} }
private void onJoinDefaultServerClick(View v){ private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
new GetInstance() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance result){
if(getActivity()==null) if(getActivity()==null)
return; return;
if(!result.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result)); args.putParcelable("instance", Parcels.wrap(result));
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
@@ -115,7 +142,7 @@ public class SplashFragment extends AppKitFragment{
} }
}) })
.wrapProgress(getActivity(), R.string.loading_instance, true) .wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(DEFAULT_SERVER); .execNoAuth(chosenDefaultServer);
} }
private void onLearnMoreClick(View v){ private void onLearnMoreClick(View v){
@@ -168,4 +195,54 @@ public class SplashFragment extends AppKitFragment{
super.onHidden(); super.onHidden();
motionEffect.deactivate(); motionEffect.deactivate();
} }
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogDefaultInstance> result){
if(result.isEmpty()){
setChosenDefaultServer(DEFAULT_SERVER);
return;
}
float sum=0f;
for(CatalogDefaultInstance inst:result){
sum+=inst.weight;
}
if(sum<=0)
sum=1f;
for(CatalogDefaultInstance inst:result){
inst.weight/=sum;
}
float rand=ThreadLocalRandom.current().nextFloat();
float prev=0f;
for(CatalogDefaultInstance inst:result){
if(rand>=prev && rand<prev+inst.weight){
setChosenDefaultServer(inst.domain);
return;
}
prev+=inst.weight;
}
// Just in case something didn't add up
setChosenDefaultServer(result.get(result.size()-1).domain);
}
@Override
public void onError(ErrorResponse error){
setChosenDefaultServer(DEFAULT_SERVER);
}
})
.execNoAuth("");
}
private void setChosenDefaultServer(String domain){
chosenDefaultServer=domain;
loadingDefaultServer=false;
if(defaultServerButton!=null && getActivity()!=null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
}
}
} }

View File

@@ -7,7 +7,7 @@ import android.view.View;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory; import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -57,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null); List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
int idx=data.indexOf(s); int idx=data.indexOf(s);
if(idx>=0){ if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -160,7 +160,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return null; return null;
} }

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
@@ -8,6 +7,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
@@ -15,8 +15,9 @@ import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -31,16 +32,20 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
public abstract class StatusListFragment extends BaseStatusListFragment<Status> implements DomainDisplay{ public abstract class StatusListFragment extends BaseStatusListFragment<Status> {
protected EventListener eventListener=new EventListener(); protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
boolean addFooter = !GlobalUserPreferences.spectatorMode || boolean isMainThreadStatus = this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id);
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id)); int flags = 0;
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext()); if (GlobalUserPreferences.spectatorMode)
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
if (!getLocalPrefs().showEmojiReactionsInLists)
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
} }
protected abstract Filter.FilterContext getFilterContext(); protected abstract FilterContext getFilterContext();
@Override @Override
protected void addAccountToKnown(Status s){ protected void addAccountToKnown(Status s){
@@ -79,7 +84,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
}); });
return; return;
} }
status.filterRevealed = true; status.filterRevealed=true;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone())); args.putParcelable("status", Parcels.wrap(status.clone()));
@@ -88,26 +93,26 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
Nav.go(getActivity(), ThreadFragment.class, args); Nav.go(getActivity(), ThreadFragment.class, args);
} }
protected void onStatusCreated(StatusCreatedEvent ev){} protected void onStatusCreated(Status status){}
protected void onStatusUpdated(StatusUpdatedEvent ev){ protected void onStatusUpdated(Status status){
ArrayList<Status> statusesForDisplayItems=new ArrayList<>(); ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
for(int i=0;i<data.size();i++){ for(int i=0;i<data.size();i++){
Status s=data.get(i); Status s=data.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){ if(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=ev.status; s.reblog=status.clone();
statusesForDisplayItems.add(s); statusesForDisplayItems.add(s);
}else if(s.id.equals(ev.status.id)){ }else if(s.id.equals(status.id)){
data.set(i, ev.status); data.set(i, status);
statusesForDisplayItems.add(ev.status); statusesForDisplayItems.add(status);
} }
} }
for(int i=0;i<preloadedData.size();i++){ for(int i=0;i<preloadedData.size();i++){
Status s=preloadedData.get(i); Status s=preloadedData.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){ if(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=ev.status; s.reblog=status.clone();
}else if(s.id.equals(ev.status.id)){ }else if(s.id.equals(status.id)){
preloadedData.set(i, ev.status); preloadedData.set(i, status);
} }
} }
@@ -225,12 +230,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status s:data){ for(Status s:data){
if(s.getContentStatus().id.equals(ev.id)){ if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev); s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
for(int i=0;i<list.getChildCount();i++){ for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i)); RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){ if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind(); footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){ }else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind(); footer.rebind();
}else if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && ev.viewHolder!=holder){
reactions.rebind();
} }
} }
} }
@@ -238,6 +246,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status s:preloadedData){ for(Status s:preloadedData){
if(s.getContentStatus().id.equals(ev.id)){ if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev); s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
} }
} }
} }
@@ -256,12 +265,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
public void onStatusCreated(StatusCreatedEvent ev){ public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID)) if(!ev.accountID.equals(accountID))
return; return;
StatusListFragment.this.onStatusCreated(ev); StatusListFragment.this.onStatusCreated(ev.status.clone());
} }
@Subscribe @Subscribe
public void onStatusUpdated(StatusUpdatedEvent ev){ public void onStatusUpdated(StatusUpdatedEvent ev){
StatusListFragment.this.onStatusUpdated(ev); StatusListFragment.this.onStatusUpdated(ev.status);
} }
@Subscribe @Subscribe
@@ -271,7 +280,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status status:data){ for(Status status:data){
Status contentStatus=status.getContentStatus(); Status contentStatus=status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){ if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(status.id, status, ev.poll); updatePoll(status.id, contentStatus, ev.poll);
} }
} }
} }

View File

@@ -17,7 +17,7 @@ import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext; import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
@@ -152,6 +152,26 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}).exec(accountID); }).exec(accountID);
} }
private void restoreStatusStates(List<Status> newData, Map<String, Status> oldData) {
for (Status s : newData) {
if (s == mainStatus) continue;
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.sensitiveRevealed = oldStatus.sensitiveRevealed;
s.filterRevealed = oldStatus.filterRevealed;
}
if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText)) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = mainStatus.spoilerRevealed;
}
}
}
}
protected void maybeApplyContext() { protected void maybeApplyContext() {
if (!transitionFinished || result == null || getContext() == null) return; if (!transitionFinished || result == null || getContext() == null) return;
Map<String, Status> oldData = null; Map<String, Status> oldData = null;
@@ -170,6 +190,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
result.descendants=filterStatuses(result.descendants); result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors); result.ancestors=filterStatuses(result.ancestors);
restoreStatusStates(result.descendants, oldData);
restoreStatusStates(result.ancestors, oldData);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) { for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i); ancestryMap.put(i.status.id, i);
@@ -178,8 +200,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
if(footerProgress!=null) if(footerProgress!=null)
footerProgress.setVisibility(View.GONE); footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants); data.addAll(result.descendants);
int prevCount=displayItems.size(); int prevCount=displayItems.size();
onAppendItems(result.descendants); onAppendItems(result.descendants);
int count=displayItems.size(); int count=displayItems.size();
if(!refreshing) if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount); adapter.notifyItemRangeInserted(prevCount, count-prevCount);
@@ -190,22 +214,6 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
count--; count--;
} }
for (Status s : data) {
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.filterRevealed = oldStatus.filterRevealed;
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText) &&
mainStatus.spoilerRevealed) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = true;
}
}
}
dataLoaded(); dataLoaded();
if(refreshing){ if(refreshing){
refreshDone(); refreshDone();
@@ -222,13 +230,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
result = null; result = null;
} }
protected Object maybeApplyMainStatus() { protected Object maybeApplyMainStatus() {
if (updatedStatus == null || !contextInitiallyRendered) return null; if (updatedStatus == null || !contextInitiallyRendered) return null;
// restore revealed states for main status because it gets updated after doLoadData // restore revealed states for main status because it gets updated after doLoadData
updatedStatus.filterRevealed = mainStatus.filterRevealed; updatedStatus.filterRevealed = mainStatus.filterRevealed;
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed; updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
updatedStatus.sensitiveRevealed = mainStatus.sensitiveRevealed;
// returning fired event object to facilitate testing // returning fired event object to facilitate testing
Object event; Object event;
@@ -352,9 +360,9 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}); });
} }
protected void onStatusCreated(StatusCreatedEvent ev){ protected void onStatusCreated(Status status){
if (ev.status.inReplyToId == null) return; if (status.inReplyToId == null) return;
Status repliedToStatus = getStatusByID(ev.status.inReplyToId); Status repliedToStatus = getStatusByID(status.inReplyToId);
if (repliedToStatus == null) return; if (repliedToStatus == null) return;
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id); NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
@@ -400,12 +408,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
} }
// update replied-to status' ancestry // update replied-to status' ancestry
if (ancestry != null) ancestry.descendantNeighbor = ev.status; if (ancestry != null) ancestry.descendantNeighbor = status;
// add ancestry for newly created status before building its display items // add ancestry for newly created status before building its display items
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus)); ancestryMap.put(status.id, new NeighborAncestryInfo(status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status)); displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(status));
data.add(nextDataIndex, ev.status); data.add(nextDataIndex, status);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
@@ -426,8 +434,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.THREAD; return FilterContext.THREAD;
} }
@Override @Override

View File

@@ -1,44 +1,22 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -48,21 +26,16 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri { public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<AccountViewModel> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID; protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>(); protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -71,6 +44,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super(40); super(40);
} }
public BaseAccountListFragment(int layout, int perPage){
super(layout, perPage);
}
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -78,9 +55,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
} }
@Override @Override
protected void onDataLoaded(List<AccountItem> d, boolean more){ protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if (getActivity() == null)
return;
if(refreshing){ if(refreshing){
relationships.clear(); relationships.clear();
} }
@@ -97,7 +72,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onRefresh(); super.onRefresh();
} }
protected void loadRelationships(List<AccountItem> accounts){ protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet()); Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids); GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req); relationshipsRequests.add(req);
@@ -134,10 +109,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false); list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1,
Math.round(16f + 56f * getResources().getConfiguration().fontScale), 16));
updateToolbar(); updateToolbar();
} }
@@ -170,6 +142,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom()); list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom()); insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{ }else{
list.setPadding(0, V.dp(16), 0, V.dp(16)); list.setPadding(0, V.dp(16), 0, V.dp(16));
@@ -187,6 +161,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon())); assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
} }
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){
super(imgLoader); super(imgLoader);
@@ -195,7 +171,9 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@NonNull @NonNull
@Override @Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder(); AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder);
return holder;
} }
@Override @Override
@@ -216,225 +194,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@Override @Override
public ImageLoaderRequest getImageRequest(int position, int image){ public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position); AccountViewModel item=data.get(position);
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1); return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
} }
} }
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username;
private final ImageView avatar;
private final Button button;
private final PopupMenu contextMenu;
private final View menuAnchor;
public AccountViewHolder(){
super(getActivity(), R.layout.item_account_list, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
avatar=findViewById(R.id.avatar);
button=findViewById(R.id.button);
menuAnchor=findViewById(R.id.menu_anchor);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
button.setOnClickListener(this::onButtonClick);
contextMenu=new PopupMenu(getActivity(), menuAnchor);
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(rel, button);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@Override
public boolean onLongClick(){
return false;
}
@Override
public boolean onLongClick(float x, float y){
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
if(relationship.following){
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
hideBoosts.setVisible(true);
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
manageUserLists.setVisible(true);
}else{
hideBoosts.setVisible(false);
manageUserLists.setVisible(false);
}
menu.findItem(R.id.block_domain).setVisible(false);
menuAnchor.setTranslationX(x);
menuAnchor.setTranslationY(y);
contextMenu.show();
return true;
}
private void onButtonClick(View v){
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.loading));
progress.setCancelable(false);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
itemView.setHasTransientState(progressShown);
if(progressShown)
progress.show();
else
progress.dismiss();
}, result->{
relationships.put(item.account.id, result);
bindRelationship();
});
}
private boolean onContextMenuItemSelected(MenuItem item){
Relationship relationship=relationships.get(this.item.account.id);
if(relationship==null)
return false;
Account account=this.item.account;
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.soft_block){
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
if (getActivity() == null) return;
bindRelationship();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListsFragment.class, args);
}
return true;
}
private void updateRelationship(Relationship r){
relationships.put(item.account.id, r);
bindRelationship();
}
}
protected static class AccountItem{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
public final CharSequence parsedName;
public AccountItem(Account account){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
}
} }

View File

@@ -0,0 +1,105 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private String currentQuery;
private boolean resultDelivered;
private SearchViewHelper searchViewHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRefreshEnabled(false);
setEmptyText("");
dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setOnClickListener(this::onItemClick);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
}
private void onItemClick(AccountViewHolder holder){
if(resultDelivered)
return;
resultDelivered=true;
Bundle res=new Bundle();
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
setResult(true, res);
Nav.finish(this, false);
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
}
@Override
public Uri getWebUri(Uri.Builder base) {
return null;
}
}

View File

@@ -9,10 +9,12 @@ import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -125,17 +127,21 @@ public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFra
@Override @Override
public void onSuccess(HeaderPaginationList<Account> result){ public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0; boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data; Collection<AccountViewModel> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else
nextMaxID=null; nextMaxID=null;
if (getActivity() == null) return; if (getActivity() == null) return;
List<AccountItem> items = result.stream() List<AccountViewModel> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream() .filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url))) .noneMatch(i -> i.account.url.equals(a.url)))
.map(AccountItem::new) .peek(account ->{
if (account.getDomainFromURL().equals(getRemoteDomain()))
account.acct=account.getFullyQualifiedName();
})
.map(a->new AccountViewModel(a, accountID))
.collect(Collectors.toList()); .collect(Collectors.toList());
boolean hasMore = nextMaxID != null; boolean hasMore = nextMaxID != null;

View File

@@ -0,0 +1,97 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.PleromaGetStatusReactions;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEmojiReactionsListFragment extends BaseAccountListFragment {
private String id;
private String emojiName;
private String url;
private int count;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id = getArguments().getString("statusID");
emojiName = getArguments().getString("emoji");
url = getArguments().getString("url");
count = getArguments().getInt("count");
SpannableStringBuilder title = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.sk_users_reacted_with, count,
count, url == null ? emojiName : ":"+emojiName+":"));
if (url != null) {
Emoji emoji = new Emoji();
emoji.shortcode = emojiName;
emoji.url = url;
HtmlParser.parseCustomEmoji(title, Collections.singletonList(emoji));
}
setTitle(title);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (url != null) {
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
}
}
@Override
public void dataLoaded() {
super.dataLoaded();
footerProgress.setVisibility(View.GONE);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest = new PleromaGetStatusReactions(id, emojiName)
.setCallback(new SimpleCallback<>(StatusEmojiReactionsListFragment.this){
@Override
public void onSuccess(List<EmojiReaction> result) {
if (getActivity() == null)
return;
List<AccountViewModel> items = result.get(0).accounts.stream()
.map(a -> new AccountViewModel(a, accountID))
.collect(Collectors.toList());
onDataLoaded(items);
}
@Override
public void onError(ErrorResponse error) {
super.onError(error);
}
})
.exec(accountID);
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
@Override
public Uri getWebUri(Uri.Builder base) {
return null;
}
}

View File

@@ -2,12 +2,12 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline; import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate; import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -16,20 +16,27 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class BubbleTimelineFragment extends StatusListFragment { public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE); private DiscoverInfoBannerHelper bannerHelper;
private String maxID; private String maxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE, accountID);
}
@Override @Override
protected boolean wantsComposeButton() { protected boolean wantsComposeButton() {
return true; return true;
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count) currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
@@ -44,14 +51,16 @@ public class BubbleTimelineFragment extends StatusListFragment {
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ protected RecyclerView.Adapter<?> getAdapter(){
super.onViewCreated(view, savedInstanceState); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return FilterContext.PUBLIC;
} }
@Override @Override

View File

@@ -17,8 +17,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions; import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowSuggestion; import org.joinmastodon.android.model.FollowSuggestion;
@@ -52,7 +52,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri { public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;
@@ -243,7 +243,7 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount))); followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount))); followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount))); postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE); followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE); followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
@@ -253,7 +253,7 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
actionWrap.setVisibility(View.GONE); actionWrap.setVisibility(View.GONE);
}else{ }else{
actionWrap.setVisibility(View.VISIBLE); actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(relationship, actionButton); UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
} }
} }

View File

@@ -1,67 +1,60 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.app.FragmentTransaction;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2; import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent { public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
private static final int QUERY_RESULT=937;
private TabLayout tabLayout; private TabLayout tabLayout;
private ViewPager2 pager; private ViewPager2 pager;
private FrameLayout[] tabViews; private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator; private TabLayoutMediator tabLayoutMediator;
private EditText searchEdit;
private boolean searchActive; private boolean searchActive;
private FrameLayout searchView; private FrameLayout searchView;
private ImageButton searchBack, searchClear; private ImageButton searchBack;
private ProgressBar searchProgress; private TextView searchText;
private View tabsDivider;
private DiscoverPostsFragment postsFragment; private DiscoverPostsFragment postsFragment;
private DiscoverHashtagsFragment hashtagsFragment; private TrendingHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment; private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment; private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment; private SearchFragment searchFragment;
private String accountID; private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced; private String currentQuery;
private boolean disableDiscover;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -95,12 +88,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabViews[i]=tabView; tabViews[i]=tabView;
} }
tabLayout.setTabTextSize(V.dp(16)); tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); tabLayout.setTabTextSize(V.dp(14));
UiUtils.reduceSwipeSensitivity(pager);
pager.setOffscreenPageLimit(4); pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter()); pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override @Override
@@ -123,7 +114,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment(); postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args); postsFragment.setArguments(args);
hashtagsFragment=new DiscoverHashtagsFragment(); hashtagsFragment=new TrendingHashtagsFragment();
hashtagsFragment.setArguments(args); hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment(); newsFragment=new DiscoverNewsFragment();
@@ -132,10 +123,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment(); accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args); accountsFragment.setArguments(args);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_hashtags, hashtagsFragment) .add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_news, newsFragment) .add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment) .add(R.id.discover_users, accountsFragment)
.commit(); .commit();
@@ -151,7 +141,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.string.for_you; case 3 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position); default -> throw new IllegalStateException("Unexpected value: "+position);
}); });
tab.view.textView.setAllCaps(true);
} }
}); });
tabLayoutMediator.attach(); tabLayoutMediator.attach();
@@ -168,62 +157,54 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
} }
}); });
searchEdit=view.findViewById(R.id.search_edit); disableDiscover=getArguments().getBoolean("disableDiscover");
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.VISIBLE);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.INVISIBLE);
}
}
});
searchView=view.findViewById(R.id.search_fragment); searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){ if(searchFragment==null){
searchFragment=new SearchFragment(); searchFragment=new SearchFragment();
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
searchFragment.setArguments(args); searchFragment.setArguments(args);
searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit(); getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
} }
searchBack=view.findViewById(R.id.search_back); searchBack=view.findViewById(R.id.search_back);
searchClear=view.findViewById(R.id.search_clear); searchText=view.findViewById(R.id.search_text);
searchProgress=view.findViewById(R.id.search_progress);
searchBack.setEnabled(searchActive);
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO); searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
searchBack.setOnClickListener(v->exitSearch()); searchBack.setOnClickListener(v->{
if(searchActive){ if(searchActive) exitSearch(); else openSearch();
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular); });
if(searchActive) searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
else searchBack.setEnabled(false);
if(searchActive || disableDiscover){
pager.setVisibility(View.GONE); pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE); tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE); searchView.setVisibility(View.VISIBLE);
} }
searchClear.setOnClickListener(v->{
searchEdit.setText(""); View searchWrap=view.findViewById(R.id.search_wrap);
searchEdit.removeCallbacks(searchDebouncer); searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
onSearchChangedDebounced(); searchWrap.setClipToOutline(true);
}); searchText.setOnClickListener(v->openSearch());
tabsDivider=view.findViewById(R.id.tabs_divider);
return view; return view;
} }
@Override
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void openSearch() {
Bundle args=new Bundle();
args.putString("account", accountID);
if(!TextUtils.isEmpty(currentQuery)){
args.putString("query", currentQuery);
}
Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this);
}
@Override @Override
public void scrollToTop(){ public void scrollToTop(){
if(!searchActive){ if(!searchActive){
@@ -233,30 +214,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
} }
} }
@Override
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void onSelect() {
if (isOnTop()) selectSearch();
else scrollToTop();
}
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
}
public void loadData(){ public void loadData(){
if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading) if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading)
hashtagsFragment.loadData(); hashtagsFragment.loadData();
} }
private void onSearchEditFocusChanged(View v, boolean hasFocus){ private void enterSearch(){
if(!searchActive && hasFocus){ if(!searchActive){
searchActive=true; searchActive=true;
pager.setVisibility(View.GONE); pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE); tabLayout.setVisibility(View.GONE);
@@ -264,28 +228,26 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular); searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
searchBack.setEnabled(true); searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
} }
} }
private void exitSearch(){ private void exitSearch(){
if(!searchActive)
return;
searchActive=false; searchActive=false;
pager.setVisibility(View.VISIBLE); searchText.setText(R.string.sk_search_fediverse);
tabLayout.setVisibility(View.VISIBLE);
searchView.setVisibility(View.GONE);
searchEdit.clearFocus();
searchEdit.setText("");
searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular); searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular);
searchBack.setEnabled(false); searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); currentQuery=null;
if (getArguments().getBoolean("disableDiscover")) searchFragment.clear();
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override if(disableDiscover) return;
protected void onHidden(){ pager.setVisibility(View.VISIBLE);
super.onHidden(); tabLayout.setVisibility(View.VISIBLE);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0); searchView.setVisibility(View.GONE);
tabsDivider.setVisibility(View.VISIBLE);
} }
private Fragment getFragmentForPage(int page){ private Fragment getFragmentForPage(int page){
@@ -307,54 +269,43 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return false; return false;
} }
private void onSearchChangedDebounced(){
searchFragment.setQuery(searchEdit.getText().toString());
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
return true;
}
private void onSearchProgressVisibilityChanged(boolean visible){
V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE);
if(searchEdit.length()>0)
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
}
@Override @Override
public void onProvideAssistContent(AssistContent assistContent) { public void onFragmentResult(int reqCode, boolean success, Bundle result){
callFragmentToProvideAssistContent(searchActive if(reqCode==QUERY_RESULT && success){
? searchFragment enterSearch();
: getFragmentForPage(pager.getCurrentItem()), assistContent); currentQuery=result.getString("query");
SearchResult.Type type;
if(result.containsKey("filter")){
type=SearchResult.Type.values()[result.getInt("filter")];
}else{
type=null;
}
searchFragment.setQuery(currentQuery, type);
searchText.setText(currentQuery);
}
} }
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> { private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull @NonNull
@Override @Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view = tabViews[viewType]; FrameLayout view=tabViews[viewType];
((ViewGroup) view.getParent()).removeView(view); ((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE); view.setVisibility(View.VISIBLE);
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view); return new SimpleViewHolder(view);
} }
@Override @Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position) { public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
}
@Override @Override
public int getItemCount() { public int getItemCount(){
return tabViews.length; return tabViews.length;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position){
return position; return position;
} }
} }

View File

@@ -1,7 +1,7 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@@ -11,36 +11,45 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks; import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri { public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
private String accountID; private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList(); private DiscoverInfoBannerHelper bannerHelper;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS); private MergeRecyclerAdapter mergeAdapter;
private UsableRecyclerView cardsList;
private ArrayList<CardViewModel> top3=new ArrayList<>();
private CardLinksAdapter cardsAdapter;
public DiscoverNewsFragment(){ public DiscoverNewsFragment(){
super(10); super(10);
@@ -50,6 +59,7 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
} }
@Override @Override
@@ -58,11 +68,14 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Card> result){ public void onSuccess(List<Card> result){
imageRequests=result.stream() top3.clear();
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150))) top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
.collect(Collectors.toList()); cardsAdapter.notifyDataSetChanged();
if (getActivity() == null) return;
onDataLoaded(result, false); onDataLoaded(result.subList(top3.size(), result.size()).stream()
.map(card->new CardViewModel(card, 56, 56))
.collect(Collectors.toList()), false);
bannerHelper.onBannerBecameVisible();
} }
}) })
.exec(accountID); .exec(accountID);
@@ -70,14 +83,27 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
@Override @Override
protected RecyclerView.Adapter getAdapter(){ protected RecyclerView.Adapter getAdapter(){
return new LinksAdapter(); cardsList=new UsableRecyclerView(getActivity());
} cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
cardsList.setClipToPadding(false);
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
super.onViewCreated(view, savedInstanceState); outRect.right=V.dp(16);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0)); }
bannerHelper.maybeAddBanner(contentWrap); });
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
return mergeAdapter;
} }
@Override @Override
@@ -85,29 +111,17 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
public boolean isOnTop() { private final List<CardViewModel> data;
return isRecyclerViewOnTop(list);
}
@Override public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){
super(imgLoader); super(imgLoader);
this.data=data;
} }
@NonNull @NonNull
@Override @Override
public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkViewHolder(); return new LinkViewHolder();
} }
@@ -117,46 +131,51 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
} }
@Override @Override
public void onBindViewHolder(LinkViewHolder holder, int position){ public void onBindViewHolder(BaseLinkViewHolder holder, int position){
holder.bind(data.get(position)); holder.bind(data.get(position).card);
super.onBindViewHolder(holder, position); super.onBindViewHolder(holder, position);
} }
@Override @Override
public int getImageCountForItem(int position){ public int getImageCountForItem(int position){
return imageRequests.get(position)==null ? 0 : 1; return data.get(position).imageRequest==null ? 0 : 1;
} }
@Override @Override
public ImageLoaderRequest getImageRequest(int position, int image){ public ImageLoaderRequest getImageRequest(int position, int image){
return imageRequests.get(position); return data.get(position).imageRequest;
} }
} }
private class LinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{ private class CardLinksAdapter extends LinksAdapter{
private final TextView name, title, subtitle; public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
private final ImageView photo; super(imgLoader, data);
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkCardViewHolder();
}
}
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
protected final TextView name, title;
protected final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear; private boolean didClear;
public LinkViewHolder(){ public BaseLinkViewHolder(int layout){
super(getActivity(), R.layout.item_trending_link, list); super(getActivity(), layout, list);
name=findViewById(R.id.name); name=findViewById(R.id.name);
title=findViewById(R.id.title); title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
photo=findViewById(R.id.photo); photo=findViewById(R.id.photo);
photo.setOutlineProvider(OutlineProviders.roundedRect(2));
photo.setClipToOutline(true);
} }
@Override @Override
public void onBind(Card item){ public void onBind(Card item){
name.setText(item.providerName); name.setText(item.providerName);
title.setText(item.title); title.setText(item.title);
int num=item.history.get(0).uses;
if(item.history.size()>1)
num+=item.history.get(1).uses;
subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num));
crossfadeDrawable.setSize(item.width, item.height); crossfadeDrawable.setSize(item.width, item.height);
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder); crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f); crossfadeDrawable.setCrossfadeAlpha(0f);
@@ -183,4 +202,20 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
UiUtils.launchWebBrowser(getActivity(), item.url); UiUtils.launchWebBrowser(getActivity(), item.url);
} }
} }
private class LinkViewHolder extends BaseLinkViewHolder{
public LinkViewHolder(){
super(R.layout.item_trending_link);
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
photo.setClipToOutline(true);
}
}
private class LinkCardViewHolder extends BaseLinkViewHolder{
public LinkCardViewHolder(){
super(R.layout.item_trending_link_card);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
}
}
} }

View File

@@ -2,22 +2,27 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses; import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment { public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS); private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
}
@Override @Override
public String getDomain() { public String getDomain() {
@@ -30,26 +35,27 @@ public class DiscoverPostsFragment extends StatusListFragment {
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible();
} }
}).exec(accountID); }).exec(accountID);
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ protected RecyclerView.Adapter<?> getAdapter(){
super.onViewCreated(view, savedInstanceState); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return FilterContext.PUBLIC;
} }
@Override @Override
public Uri getWebUri(Uri.Builder base) { public Uri getWebUri(Uri.Builder base){
return isInstanceAkkoma() ? null : base.path("/explore/posts").build(); return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
} }
} }

View File

@@ -2,55 +2,59 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class FederatedTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
public class FederatedTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
private String maxID; private String maxID;
@Override @Override
protected boolean wantsComposeButton() { public void onCreate(Bundle savedInstanceState){
return true; super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE, accountID);
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count) currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; boolean empty=result.isEmpty();
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList()); AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
} }
}) })
.exec(accountID); .exec(accountID);
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ protected RecyclerView.Adapter<?> getAdapter(){
super.onViewCreated(view, savedInstanceState); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return FilterContext.PUBLIC;
} }
@Override @Override

View File

@@ -7,54 +7,59 @@ import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class LocalTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
public class LocalTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
private String maxID; private String maxID;
@Override @Override
protected boolean wantsComposeButton() { public void onCreate(Bundle savedInstanceState){
return true; super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count) currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(!result.isEmpty()) if(!result.isEmpty())
maxID=result.get(result.size()-1).id; maxID=result.get(result.size()-1).id;
if (getActivity() == null) return; boolean empty=result.isEmpty();
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList()); AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
} }
}) })
.exec(accountID); .exec(accountID);
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ protected RecyclerView.Adapter<?> getAdapter(){
super.onViewCreated(view, savedInstanceState); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(contentWrap); bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
} }
@Override @Override
protected Filter.FilterContext getFilterContext() { protected FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC; return FilterContext.PUBLIC;
} }
@Override @Override
public Uri getWebUri(Uri.Builder base) { public Uri getWebUri(Uri.Builder base){
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build(); return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
} }
} }

View File

@@ -4,10 +4,10 @@ import android.app.Activity;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults; import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -15,7 +15,7 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.model.SearchResults;
@@ -24,7 +24,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -35,25 +34,18 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult> { public class SearchFragment extends BaseStatusListFragment<SearchResult>{
private String currentQuery; private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems; private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class); private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
private List<SearchResult> unfilteredResults=Collections.emptyList(); private List<SearchResult> unfilteredResults=Collections.emptyList();
private HideableSingleViewRecyclerAdapter headerAdapter;
private ProgressVisibilityListener progressVisibilityListener;
private InputMethodManager imm; private InputMethodManager imm;
private TabLayout tabLayout;
public SearchFragment(){ public SearchFragment(){
setLayout(R.layout.fragment_search); setLayout(R.layout.fragment_search);
} }
@@ -63,8 +55,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true); setRetainInstance(true);
setEmptyText(R.string.sk_recent_searches_placeholder);
loadData(); loadData();
resetEmptyText();
} }
@Override @Override
@@ -73,16 +65,12 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
imm=activity.getSystemService(InputMethodManager.class); imm=activity.getSystemService(InputMethodManager.class);
} }
private void resetEmptyText() {
setEmptyText(R.string.sk_recent_searches_placeholder);
}
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){ protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
return switch(s.type){ return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.PUBLIC); case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, FilterContext.PUBLIC, !getLocalPrefs().showEmojiReactionsInLists ? StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS : 0);
}; };
} }
@@ -126,24 +114,24 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
if (getActivity() == null) return; GetSearchResults.Type type;
resetEmptyText(); if(currentFilter.size()==1){
if(isInRecentMode()){ type=switch(currentFilter.iterator().next()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{ case ACCOUNT -> GetSearchResults.Type.ACCOUNTS;
if(getActivity()==null) case HASHTAG -> GetSearchResults.Type.HASHTAGS;
return; case STATUS -> GetSearchResults.Type.STATUSES;
unfilteredResults=sr; };
prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false);
});
}else{ }else{
setEmptyText(R.string.sk_searching); type=null;
progressVisibilityListener.onProgressVisibilityChanged(true); }
currentRequest=new GetSearchResults(currentQuery, null, true) if(currentQuery==null){
dataLoaded();
return;
}
currentRequest=new GetSearchResults(currentQuery, type, true)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(SearchResults result){ public void onSuccess(SearchResults result){
setEmptyText(R.string.sk_no_results);
ArrayList<SearchResult> results=new ArrayList<>(); ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){ if(result.accounts!=null){
for(Account acc:result.accounts) for(Account acc:result.accounts)
@@ -159,25 +147,20 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
} }
prevDisplayItems=new ArrayList<>(displayItems); prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results; unfilteredResults=results;
if (getActivity() == null) return;
onDataLoaded(filterSearchResults(results), false); onDataLoaded(filterSearchResults(results), false);
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
resetEmptyText();
currentRequest=null; currentRequest=null;
Activity a=getActivity(); Activity a=getActivity();
if(a==null) if(a==null)
return; return;
error.showToast(a); error.showToast(a);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
} }
}) })
.exec(accountID); .exec(accountID);
} }
}
@Override @Override
public void updateList(){ public void updateList(){
@@ -186,86 +169,33 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
return; return;
} }
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType()); UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode() && !displayItems.isEmpty();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages(); imgLoader.forceUpdateImages();
prevDisplayItems=null; prevDisplayItems=null;
} }
@Override public void setQuery(String q, SearchResult.Type filter){
protected void onDataLoaded(List<SearchResult> d, boolean more){ if(q.isBlank()) {
super.onDataLoaded(d, more); setEmptyText(R.string.sk_recent_searches_placeholder);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
tabLayout=view.findViewById(R.id.tabbar);
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people));
tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags));
tabLayout.addTab(tabLayout.newTab().setText(R.string.posts));
for(int i=0;i<tabLayout.getTabCount();i++){
tabLayout.getTabAt(i).view.textView.setAllCaps(true);
}
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
setFilter(switch(tab.getPosition()){
case 0 -> EnumSet.allOf(SearchResult.Type.class);
case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT);
case 2 -> EnumSet.of(SearchResult.Type.HASHTAG);
case 3 -> EnumSet.of(SearchResult.Type.STATUS);
default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition());
});
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false);
header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header));
adapter.addAdapter(super.getAdapter());
headerAdapter.setVisible(isInRecentMode());
return adapter;
}
public void setQuery(String q){
if(Objects.equals(q, currentQuery) || q.isBlank())
return; return;
}
if(currentRequest!=null){ if(currentRequest!=null){
currentRequest.cancel(); currentRequest.cancel();
currentRequest=null; currentRequest=null;
} }
currentQuery=q; currentQuery=q;
setEmptyText(R.string.no_search_results);
if(filter==null)
currentFilter=EnumSet.allOf(SearchResult.Type.class);
else
currentFilter=EnumSet.of(filter);
refreshing=true; refreshing=true;
doLoadData(0, 0); loadData();
} }
private void setFilter(EnumSet<SearchResult.Type> filter){ private void setFilter(EnumSet<SearchResult.Type> filter){
if(filter.equals(currentFilter)) if(filter.equals(currentFilter))
return; return;
currentFilter=filter; currentFilter=filter;
if(isInRecentMode())
return;
// This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy // This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy
prevDisplayItems=new ArrayList<>(displayItems); prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true; refreshing=true;
@@ -287,23 +217,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
return null; return null;
} }
private void onClearRecentClick(View v){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
onDataLoaded(unfilteredResults=Collections.emptyList(), false);
}
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){
this.progressVisibilityListener=progressVisibilityListener;
}
@Override @Override
public void onScrollStarted(){ public void onScrollStarted(){
super.onScrollStarted(); super.onScrollStarted();
@@ -312,6 +225,13 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
} }
} }
public void clear() {
data.clear();
preloadedData.clear();
adapter.notifyDataSetChanged();
V.setVisibilityAnimated(content, View.GONE);
}
@Override @Override
public Uri getWebUri(Uri.Builder base) { public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search"); Uri.Builder searchUri = base.path("/search");

View File

@@ -0,0 +1,574 @@
package org.joinmastodon.android.fragments.discover;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.RoundedCorner;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
private HideableSingleViewRecyclerAdapter recentsHeader;
private ListItem<Void> openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem;
private ArrayList<ListItem<Void>> topOptions=new ArrayList<>();
private GenericListItemsAdapter<Void> topOptionsAdapter;
private String accountID;
private SearchViewHelper searchViewHelper;
private String currentQuery;
private LayerDrawable navigationIcon;
private Drawable searchIcon, backIcon;
public SearchQueryFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setRefreshEnabled(false);
setEmptyText("");
openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_fluent_link_24_regular, this::onOpenURLClick);
goToHashtagItem=new ListItem<>("", null, R.drawable.ic_fluent_number_symbol_24_regular, this::onGoToHashtagClick);
goToAccountItem=new ListItem<>("", null, R.drawable.ic_fluent_person_24_regular, this::onGoToAccountClick);
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_fluent_search_24_regular, this::onGoToStatusSearchClick);
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_fluent_people_24_regular, this::onGoToAccountSearchClick);
currentQuery=getArguments().getString("query");
dataLoaded();
doLoadData(0, 0);
}
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{
if(getActivity()==null)
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
}).collect(Collectors.toList()), false);
recentsHeader.setVisible(!data.isEmpty());
});
}else{
currentRequest=new GetSearchResults(currentQuery, null, false)
.limit(2)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
})
.collect(Collectors.toList()), false);
recentsHeader.setVisible(false);
}
})
.exec(accountID);
}
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false);
TextView title=header.findViewById(R.id.title);
Button action=header.findViewById(R.id.action_btn);
title.setText(R.string.recent_searches);
action.setText(R.string.clear_all);
action.setOnClickListener(v->onClearRecentClick());
recentsHeader=new HideableSingleViewRecyclerAdapter(header);
mergeAdapter.addAdapter(recentsHeader);
mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions));
mergeAdapter.addAdapter(new SearchResultsAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.sk_search_fediverse));
searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce);
searchViewHelper.addDivider(contentView);
searchViewHelper.setEnterCallback(this::onSearchViewEnter);
navigationIcon=new LayerDrawable(new Drawable[]{
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_fluent_search_24_regular, getToolbarContext().getTheme()).mutate(),
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_fluent_arrow_left_24_regular, getToolbarContext().getTheme()).mutate()
}){
@Override
public Drawable mutate(){
return this;
}
};
super.onViewCreated(view, savedInstanceState);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
view.setBackgroundColor(color);
setStatusBarColor(color);
setNavigationBarColor(color);
if(currentQuery!=null){
searchViewHelper.setQuery(currentQuery);
searchIcon.setAlpha(0);
}
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8);
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
refreshing=true;
doLoadData(0, 0);
}
private void onQueryChangedNoDebounce(String q){
updateTopOptions(q);
if(!TextUtils.isEmpty(q)){
recentsHeader.setVisible(false);
}
data.clear();
mergeAdapter.notifyDataSetChanged();
}
private void updateTopOptions(String q){
topOptions.clear();
// https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233
String trimmedValue=q.trim();
if(trimmedValue.length()>0){
boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" ");
if(couldBeURL){
topOptions.add(openUrlItem);
}
boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find();
if(couldBeHashtag){
String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue;
goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag);
topOptions.add(goToHashtagItem);
}
Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue);
if(usernameMatcher.find()){
String username="@"+usernameMatcher.group(1);
String atDomain=usernameMatcher.group(2);
if(atDomain==null){
username+="@"+AccountSessionManager.get(accountID).domain;
}
goToAccountItem.title=getString(R.string.search_go_to_account, username);
topOptions.add(goToAccountItem);
}
goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue);
topOptions.add(goToStatusSearchItem);
goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue);
topOptions.add(goToAccountSearchItem);
}
topOptionsAdapter.notifyDataSetChanged();
}
@Override
public Animator onCreateEnterTransition(View prev, View container){
return createTransition(prev, container, true);
}
@Override
public Animator onCreateExitTransition(View prev, View container){
return createTransition(prev, container, false);
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected Drawable getNavigationIconDrawable(){
return navigationIcon;
}
@Override
protected void onShown(){
super.onShown();
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchViewHelper.getSearchEdit(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
}
@RequiresApi(api = Build.VERSION_CODES.S)
private float getScreenCornerRadius(WindowInsets insets, int pos){
RoundedCorner corner=insets.getRoundedCorner(pos);
if(corner==null)
return 0;
return corner.getRadius();
}
private Animator createTransition(View prev, View container, boolean enter){
int[] loc={0, 0};
View searchBtn=prev.findViewById(R.id.search_wrap);
searchBtn.getLocationInWindow(loc);
int btnLeft=loc[0], btnTop=loc[1];
container.getLocationInWindow(loc);
int offX=btnLeft-loc[0], offY=btnTop-loc[1];
float screenRadius;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
WindowInsets insets=container.getRootWindowInsets();
screenRadius=Math.min(
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)),
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT))
);
}else{
screenRadius=0;
}
float buttonRadius=V.dp(26);
Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight());
Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight());
AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius);
container.setOutlineProvider(outlineProvider);
container.setClipToOutline(true);
AnimatorSet set=new AnimatorSet();
ObjectAnimator boundsAnim;
Toolbar toolbar=getToolbar();
float toolbarTX=offX-toolbar.getX();
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
ArrayList<Animator> anims=new ArrayList<>();
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
View parentContent=prev.findViewById(R.id.discover_content);
View parentContentParent=(View) parentContent.getParent();
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
if(enter){
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
}else{
}
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
for(Animator anim:anims){
anim.setDuration(enter ? 700 : 300);
}
if(TextUtils.isEmpty(currentQuery)){
anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200));
anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200));
}
ObjectAnimator parentContentFade;
anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250));
if(!enter){
parentContentFade.setStartDelay(50);
ObjectAnimator parentContentTY;
anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250));
parentContentTY.setStartDelay(50);
}
set.playTogether(anims);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate));
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
container.setOutlineProvider(null);
container.setClipToOutline(false);
parentContentParent.setBackground(null);
}
});
boundsAnim.addUpdateListener(animation->{
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
return set;
}
private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
private void onSearchViewEnter(){
deliverResult(currentQuery, null);
}
private void onOpenURLClick(){
UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false);
}
private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q, null);
}
private void onGoToAccountClick(){
String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){
q="@"+q;
}
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
UiUtils.lookupAccountHandle(getContext(), accountID, q, (clazz, args) -> {
if (!args.containsKey("profileAccount")) {
Toast.makeText(getContext(), R.string.no_search_results, Toast.LENGTH_SHORT).show();
return;
}
Nav.go((Activity) getContext(), ProfileFragment.class, args);
}).ifPresent(progress -> progress.wrapProgress((Activity) getContext(), R.string.loading, true));
}
private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
}
private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
}
private void onClearRecentClick(){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
data.clear();
recentsHeader.setVisible(false);
mergeAdapter.notifyDataSetChanged();
}
}
private void deliverResult(String query, SearchResult.Type typeFilter){
Bundle res=new Bundle();
res.putString("query", query);
if(typeFilter!=null)
res.putInt("filter", typeFilter.ordinal());
setResult(true, res);
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;
private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){
this.boundsFrom=boundsFrom;
this.boundsTo=boundsTo;
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(
UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction),
UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction),
UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction),
UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction),
radius
);
}
@Keep
public float getBoundsFraction(){
return boundsFraction;
}
@Keep
public void setBoundsFraction(float boundsFraction){
this.boundsFraction=boundsFraction;
}
@Keep
public float getRadius(){
return radius;
}
@Keep
public void setRadius(float radius){
this.radius=radius;
}
}
private class SearchResultsAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
public SearchResultsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
if(viewType==R.id.list_item_account){
return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null);
}else if(viewType==R.id.list_item_simple){
return new SimpleListItemViewHolder(parent.getContext(), parent);
}
throw new IllegalArgumentException();
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){
if(holder instanceof CustomAccountViewHolder avh){
avh.bind(data.get(position).account);
avh.searchResult=data.get(position).result;
}else if(holder instanceof SimpleListItemViewHolder ivh){
ivh.bind(data.get(position).hashtagItem);
}
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getItemViewType(int position){
return switch(data.get(position).result.type){
case ACCOUNT -> R.id.list_item_account;
case HASHTAG -> R.id.list_item_simple;
default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type);
};
}
@Override
public int getImageCountForItem(int position){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null)
return vm.account.emojiHelper.getImageCount()+1;
return 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null){
if(image==0)
return vm.account.avaRequest;
return vm.account.emojiHelper.getImageRequest(image-1);
}
return null;
}
}
private class CustomAccountViewHolder extends AccountViewHolder{
public SearchResult searchResult;
public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment, list, relationships);
setStyle(AccessoryType.NONE, false);
}
@Override
public void onClick(){
super.onClick();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult);
}
}
}

View File

@@ -1,9 +1,5 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -11,29 +7,26 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags; import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView; import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List; import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri { public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
private String accountID; private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public DiscoverHashtagsFragment(){ public TrendingHashtagsFragment(){
super(10); super(10);
} }
@@ -49,7 +42,6 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Hashtag> result){ public void onSuccess(List<Hashtag> result){
if (getActivity() == null) return;
onDataLoaded(result, false); onDataLoaded(result, false);
} }
}) })
@@ -61,33 +53,11 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
return new HashtagsAdapter(); return new HashtagsAdapter();
} }
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16));
bannerHelper.maybeAddBanner(contentWrap);
}
@Override @Override
public void scrollToTop(){ public void scrollToTop(){
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{ private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull @NonNull
@Override @Override
@@ -123,11 +93,9 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
if (item.history == null || item.history.isEmpty()) { if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null); subtitle.setText(null);
chart.setVisibility(View.GONE); chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return; return;
} }
chart.setVisibility(View.VISIBLE); chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts; int numPeople=item.history.get(0).accounts;
if(item.history.size()>1) if(item.history.size()>1)
numPeople+=item.history.get(1).accounts; numPeople+=item.history.get(1).accounts;

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo; import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment; import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment; import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends ToolbarFragment{ public class AccountActivationFragment extends ToolbarFragment{
private String accountID; private String accountID;
@@ -50,6 +47,7 @@ public class AccountActivationFragment extends ToolbarFragment{
private APIRequest currentRequest; private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer; private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime; private long lastResendTime;
private boolean visible;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -70,7 +68,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{ openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args); Nav.go(getActivity(), SettingsMainFragment.class, args);
return true; return true;
}); });
resendBtn=view.findViewById(R.id.btn_resend); resendBtn=view.findViewById(R.id.btn_resend);
@@ -108,24 +106,20 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override @Override
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){ super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(contentView, insets));
int inset=insets.getSystemWindowInsetBottom();
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
} }
@Override @Override
protected void onShown(){ protected void onShown(){
super.onShown(); super.onShown();
visible=true;
tryGetAccount(); tryGetAccount();
} }
@Override @Override
protected void onHidden(){ protected void onHidden(){
super.onHidden(); super.onHidden();
visible=false;
if(currentRequest!=null){ if(currentRequest!=null){
currentRequest.cancel(); currentRequest.cancel();
currentRequest=null; currentRequest=null;
@@ -238,6 +232,8 @@ public class AccountActivationFragment extends ToolbarFragment{
} }
private void proceed(){ private void proceed(){
if(!visible)
return;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
// Nav.goClearingStack(getActivity(), HomeFragment.class, args); // Nav.goClearingStack(getActivity(), HomeFragment.class, args);

Some files were not shown because too many files have changed in this diff Show More