diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index 99b0beb480..f7610292bf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -37,6 +37,8 @@ public abstract class MastodonAPIRequest extends APIRequest{ private static final String TAG="MastodonAPIRequest"; + private static MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null); + private String domain; private AccountSession account; private String path; @@ -95,14 +97,14 @@ public MastodonAPIRequest exec(String accountID){ public MastodonAPIRequest execNoAuth(String domain){ this.domain=domain; - AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this); + unauthenticatedApiController.submitRequest(this); return this; } public MastodonAPIRequest exec(String domain, Token token){ this.domain=domain; this.token=token; - AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this); + unauthenticatedApiController.submitRequest(this); return this; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java b/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java index 0526ab79b5..e7f89590da 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java @@ -8,14 +8,23 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited; import org.joinmastodon.android.api.requests.statuses.SetStatusMuted; import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; +import org.joinmastodon.android.api.session.AccountSession; +import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent; import org.joinmastodon.android.events.ReblogDeletedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent; -import org.joinmastodon.android.events.StatusDeletedEvent; +import org.joinmastodon.android.model.Emoji; +import org.joinmastodon.android.model.EmojiCategory; +import org.joinmastodon.android.model.EmojiReaction; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.StatusPrivacy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import me.grishka.appkit.api.Callback; @@ -42,6 +51,9 @@ public void setFavorited(Status status, boolean favorited, Consumer cb){ if(!Looper.getMainLooper().isCurrentThread()) throw new IllegalStateException("Can only be called from main thread"); + AccountSession session=AccountSessionManager.get(accountID); + Instance instance=session.getInstance().get(); + SetStatusFavorited current=runningFavoriteRequests.remove(status.id); if(current!=null){ current.cancel(); @@ -54,6 +66,7 @@ public void onSuccess(Status result){ result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1)); cb.accept(result); if(updateCounters) E.post(new StatusCountersUpdatedEvent(result)); + if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, result.reactions, false, null)); } @Override @@ -63,12 +76,58 @@ public void onError(ErrorResponse error){ status.favourited=!favorited; cb.accept(status); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status)); + if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, status.reactions, false, null)); } }) .exec(accountID); runningFavoriteRequests.put(status.id, req); status.favourited=favorited; if(updateCounters) E.post(new StatusCountersUpdatedEvent(status)); + + if(instance.configuration==null || instance.configuration.reactions==null) + return; + + String defaultReactionEmojiRaw=instance.configuration.reactions.defaultReaction; + if(!instance.isIceshrimpJs() || defaultReactionEmojiRaw==null) + return; + + boolean reactionIsCustom=defaultReactionEmojiRaw.startsWith(":"); + String defaultReactionEmoji=reactionIsCustom ? defaultReactionEmojiRaw.substring(1, defaultReactionEmojiRaw.length()-1) : defaultReactionEmojiRaw; + ArrayList reactions=new ArrayList<>(status.reactions.size()); + for(EmojiReaction reaction:status.reactions){ + reactions.add(reaction.copy()); + } + Optional existingReaction=reactions.stream().filter(r->r.me).findFirst(); + Optional existingDefaultReaction=reactions.stream().filter(r->r.name.equals(defaultReactionEmoji)).findFirst(); + if(existingReaction.isPresent() && !favorited){ + existingReaction.get().me=false; + existingReaction.get().count--; + existingReaction.get().pendingChange=true; + }else if(existingDefaultReaction.isPresent() && favorited){ + existingDefaultReaction.get().count++; + existingDefaultReaction.get().me=true; + existingDefaultReaction.get().pendingChange=true; + }else if(favorited){ + EmojiReaction reaction=null; + if(reactionIsCustom){ + List customEmojis=AccountSessionManager.getInstance().getCustomEmojis(session.domain); + for(EmojiCategory category:customEmojis){ + for(Emoji emoji:category.emojis){ + if(emoji.shortcode.equals(defaultReactionEmoji)){ + reaction=EmojiReaction.of(emoji, session.self); + break; + } + } + } + if(reaction==null) + reaction=EmojiReaction.of(defaultReactionEmoji, session.self); + }else{ + reaction=EmojiReaction.of(defaultReactionEmoji, session.self); + } + reaction.pendingChange=true; + reactions.add(reaction); + } + E.post(new EmojiReactionsUpdatedEvent(status.id, reactions, false, null)); } public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer cb){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java index e17136d9e4..0db74f3d0c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java @@ -16,6 +16,7 @@ import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.PushSubscription; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.TimelineDefinition; import java.lang.reflect.Type; @@ -23,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; public class AccountLocalPreferences{ private final SharedPreferences prefs; @@ -72,19 +74,20 @@ public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){ // preReplySheet=prefs.getBoolean("preReplySheet", false); // MEGALODON + Optional instance=session.getInstance(); 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); + defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", instance.map(Instance::isIceshrimp).orElse(false) ? ContentType.MISSKEY_MARKDOWN.name() : ContentType.PLAIN.name())); + contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", instance.map(i->!i.isIceshrimp()).orElse(false)); 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()); + emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", instance.map(i->i.isAkkoma() || i.isIceshrimp()).orElse(false)); showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name())); color=prefs.contains("color") ? ColorPreference.valueOf(prefs.getString("color", null)) : null; recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>()); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 58252c51a6..ea66479eab 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -70,7 +70,6 @@ public class AccountSessionManager{ private HashMap> customEmojis=new HashMap<>(); private HashMap instancesLastUpdated=new HashMap<>(); private HashMap instances=new HashMap<>(); - private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null); private Instance authenticatingInstance; private Application authenticatingApp; private String lastActiveAccountID; @@ -109,7 +108,7 @@ private AccountSessionManager(){ Log.e(TAG, "Error loading accounts", x); } lastActiveAccountID=prefs.getString("lastActiveAccount", null); - MastodonAPIController.runInBackground(()->readInstanceInfo(domains)); + readInstanceInfo(domains); maybeUpdateShortcuts(); } @@ -247,11 +246,6 @@ public void removeAccount(String id){ maybeUpdateShortcuts(); } - @NonNull - public MastodonAPIController getUnauthenticatedApiController(){ - return unauthenticatedApiController; - } - public void authenticate(Activity activity, Instance instance){ authenticatingInstance=instance; new CreateOAuthApp() diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java index b376f24dc3..e267b45b15 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java @@ -68,14 +68,14 @@ protected List buildDisplayItems(Announcement a) { instanceUser.url = "https://"+session.domain+"/about"; instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail; instanceUser.emojis = List.of(); - Status fakeStatus = a.toStatus(); + Status fakeStatus = a.toStatus(isInstanceIceshrimp()); TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true); textItem.textSelectable = true; List items=new ArrayList<>(); items.add(HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead)); items.add(textItem); - if(!isInstanceAkkoma()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true)); + if(!isInstanceAkkoma() && !isInstanceIceshrimp()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true)); return items; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 810ad2aba3..e9f66df604 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -838,6 +838,14 @@ public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){ list.invalidateItemDecorations(); } + public void onFavoriteChanged(Status status, String itemID) { + FooterStatusDisplayItem.Holder footer=findHolderOfType(itemID, FooterStatusDisplayItem.Holder.class); + if(footer!=null){ + footer.getItem().status=status; + footer.onFavoriteClick(); + } + } + @Override public String getAccountID(){ return accountID; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 0c87e48e99..716c8c6691 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -927,6 +927,9 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ } return false; }); + if(instance.isIceshrimpJs()) + languageButton.setVisibility(View.GONE); // hide language selector on Iceshrimp-JS because the feature is not supported + if (!GlobalUserPreferences.relocatePublishButton) publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth())); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java index 9934b2e17f..d8f84e5650 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java @@ -22,6 +22,14 @@ default boolean isInstancePixelfed() { return getInstance().map(Instance::isPixelfed).orElse(false); } + default boolean isInstanceIceshrimp() { + return getInstance().map(Instance::isIceshrimp).orElse(false); + } + + default boolean isInstanceIceshrimpJs() { + return getInstance().map(Instance::isIceshrimpJs).orElse(false); + } + default Optional getInstance() { return getSession().getInstance(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 1c6cbbf6d2..92d2f71438 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -24,6 +24,7 @@ import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; +import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; @@ -122,7 +123,9 @@ protected List buildDisplayItems(Notification n){ } NotificationHeaderStatusDisplayItem titleItem; - if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){ + Account self=AccountSessionManager.get(accountID).self; + if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS + || (n.type==Notification.Type.REBLOG && !n.status.account.id.equals(self.id))){ // Iceshrimp quote titleItem=null; }else{ titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID); @@ -316,13 +319,16 @@ public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){ public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){ for(Notification n : data){ if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){ - n.status.getContentStatus().update(ev); - AccountSessionManager.get(accountID).getCacheController().updateNotification(n); for(int i=0; i instance=AccountSessionManager.get(accountID).getInstance(); + disableDiscover=instance.map(Instance::isAkkoma).orElse(false); + isIceshrimp=instance.map(Instance::isIceshrimp).orElse(false); + + tabViews=new FrameLayout[isIceshrimp ? 3 : 4]; // reduce array size on Iceshrimp to hide news feed because it's unsupported and always returns an empty list for(int i=0;i R.id.discover_posts; case 1 -> R.id.discover_hashtags; - case 2 -> R.id.discover_news; + case 2 -> isIceshrimp ? R.id.discover_users : R.id.discover_news; // skip unsupported news discovery on Iceshrimp case 3 -> R.id.discover_users; default -> throw new IllegalStateException("Unexpected value: "+i); }); @@ -126,12 +135,15 @@ public void onPageSelected(int position){ accountsFragment=new DiscoverAccountsFragment(); accountsFragment.setArguments(args); - getChildFragmentManager().beginTransaction() - .add(R.id.discover_posts, postsFragment) - .add(R.id.discover_hashtags, hashtagsFragment) - .add(R.id.discover_news, newsFragment) - .add(R.id.discover_users, accountsFragment) - .commit(); + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + transaction + .add(R.id.discover_posts, postsFragment) + .add(R.id.discover_hashtags, hashtagsFragment); + if(!isIceshrimp) // skip unsupported news discovery on Iceshrimp + transaction.add(R.id.discover_news, newsFragment); + transaction + .add(R.id.discover_users, accountsFragment) + .commit(); } tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){ @@ -140,7 +152,7 @@ public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ tab.setText(switch(position){ case 0 -> R.string.posts; case 1 -> R.string.hashtags; - case 2 -> R.string.news; + case 2 -> isIceshrimp ? R.string.for_you : R.string.news; // skip unsupported news discovery on Iceshrimp case 3 -> R.string.for_you; default -> throw new IllegalStateException("Unexpected value: "+position); }); @@ -160,7 +172,6 @@ public void onTabReselected(TabLayout.Tab tab){ } }); - disableDiscover=AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false); searchView=view.findViewById(R.id.search_fragment); if(searchFragment==null){ searchFragment=new SearchFragment(); @@ -262,7 +273,7 @@ private Fragment getFragmentForPage(int page){ return switch(page){ case 0 -> postsFragment; case 1 -> hashtagsFragment; - case 2 -> newsFragment; + case 2 -> isIceshrimp ? accountsFragment : newsFragment; // skip unsupported news discovery on Iceshrimp case 3 -> accountsFragment; default -> throw new IllegalStateException("Unexpected value: "+page); }; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java index 4246c0afcd..a1c217e4ea 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java @@ -54,7 +54,6 @@ public void onCreate(Bundle savedInstanceState){ languageResolver.from(s.preferences.postingDefaultLanguage).orElse(null); List> items = new ArrayList<>(List.of( - languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick), customTabsItem=new ListItem<>(getString(R.string.settings_custom_tabs), getString(GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser), R.drawable.ic_fluent_open_24_regular, this::onCustomTabsClick), altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, i->toggleCheckableItem(altTextItem)), showPostsWithoutAltItem=new CheckableListItem<>(R.string.mo_settings_show_posts_without_alt, R.string.mo_settings_show_posts_without_alt_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showPostsWithoutAlt, R.drawable.ic_fluent_eye_tracking_on_24_regular, i->toggleCheckableItem(showPostsWithoutAltItem)), @@ -73,6 +72,11 @@ public void onCreate(Bundle savedInstanceState){ showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, i->toggleCheckableItem(showRepliesItem)) )); + if(!isInstanceIceshrimpJs()) items.add( + 0, + languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick) + ); + if(isInstanceAkkoma()) items.add( replyVisibilityItem=new ListItem<>(R.string.sk_settings_reply_visibility, getReplyVisibilityString(), R.drawable.ic_fluent_chat_24_regular, this::onReplyVisibilityClick) ); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsInstanceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsInstanceFragment.java index 6dd90718a5..35dbd62863 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsInstanceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsInstanceFragment.java @@ -17,6 +17,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.utils.UiUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -35,24 +36,27 @@ public void onCreate(Bundle savedInstanceState){ setTitle(R.string.sk_settings_instance); AccountSession s=AccountSessionManager.get(accountID); lp=s.getLocalPreferences(); - onDataLoaded(List.of( + ArrayList> items=new ArrayList<>(List.of( new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_fluent_server_24_regular, this::onServerClick), new ListItem<>(R.string.sk_settings_profile, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/profile")), new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")), new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true), - contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, i->onContentTypeClick()), - defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true), emojiReactionsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions, R.string.sk_settings_emoji_reactions_explanation, CheckableListItem.Style.SWITCH, lp.emojiReactionsEnabled, R.drawable.ic_fluent_emoji_laugh_24_regular, i->onEmojiReactionsClick()), showEmojiReactionsItem=new ListItem<>(R.string.sk_settings_show_emoji_reactions, getShowEmojiReactionsString(), R.drawable.ic_fluent_emoji_24_regular, this::onShowEmojiReactionsClick, 0, true), localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, i->onLocalOnlyClick()), glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, i->toggleCheckableItem(glitchModeItem)) )); - contentTypesItem.checkedChangeListener=checked->onContentTypeClick(); - defaultContentTypeItem.isEnabled=contentTypesItem.checked; + if(!isInstanceIceshrimp()){ + items.add(4, contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, i->onContentTypeClick())); + items.add(5, defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true)); + contentTypesItem.checkedChangeListener=checked->onContentTypeClick(); + defaultContentTypeItem.isEnabled=contentTypesItem.checked; + } emojiReactionsItem.checkedChangeListener=checked->onEmojiReactionsClick(); showEmojiReactionsItem.isEnabled=emojiReactionsItem.checked; localOnlyItem.checkedChangeListener=checked->onLocalOnlyClick(); glitchModeItem.isEnabled=localOnlyItem.checked; + onDataLoaded(items); } @Override @@ -61,7 +65,8 @@ protected void doLoadData(int offset, int count){} @Override protected void onHidden(){ super.onHidden(); - lp.contentTypesEnabled=contentTypesItem.checked; + if(contentTypesItem!=null) + lp.contentTypesEnabled=contentTypesItem.checked; lp.emojiReactionsEnabled=emojiReactionsItem.checked; lp.localOnlySupported=localOnlyItem.checked; lp.glitchInstance=glitchModeItem.checked; @@ -84,7 +89,8 @@ private void onContentTypeClick(){ private void resetDefaultContentType(){ lp.defaultContentType=defaultContentTypeItem.isEnabled - ? ContentType.PLAIN : ContentType.UNSPECIFIED; + ? isInstanceIceshrimp() ? ContentType.MISSKEY_MARKDOWN + : ContentType.PLAIN : ContentType.UNSPECIFIED; defaultContentTypeItem.subtitleRes=lp.defaultContentType.getName(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java index 20142ebbd2..ebfef85426 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java @@ -64,7 +64,7 @@ public void onCreate(Bundle savedInstanceState){ )); Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain); - if(!instance.isAkkoma()){ + if(!instance.isAkkoma() && !instance.isIceshrimpJs()){ // hide filter settings on Akkoma and Iceshrimp-JS because the servers don't support the feature data.add(3, new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_fluent_filter_24_regular, this::onFiltersClick)); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Announcement.java b/mastodon/src/main/java/org/joinmastodon/android/model/Announcement.java index 9c769ed03f..ba8a949ed8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Announcement.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Announcement.java @@ -50,11 +50,11 @@ public void postprocess() throws ObjectValidationException{ if(reactions==null) reactions=new ArrayList<>(); } - public Status toStatus() { + public Status toStatus(boolean isIceshrimp) { Status s=Status.ofFake(id, content, publishedAt); s.createdAt=startsAt != null ? startsAt : publishedAt; s.reactions=reactions; - if(updatedAt != null) s.editedAt=updatedAt; + if(updatedAt != null && (!isIceshrimp || !updatedAt.equals(publishedAt))) s.editedAt=updatedAt; return s; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/ContentType.java b/mastodon/src/main/java/org/joinmastodon/android/model/ContentType.java index 79957bfcb4..24f1d41612 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/ContentType.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/ContentType.java @@ -34,6 +34,6 @@ public int getName() { } public boolean supportedByInstance(Instance i) { - return i.isAkkoma() || (this!=BBCODE && this!=MISSKEY_MARKDOWN); + return i.isAkkoma() || i.isIceshrimp() || (this!=BBCODE && this!=MISSKEY_MARKDOWN); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java b/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java index 4ffed6e6da..12ec162c85 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java @@ -22,6 +22,7 @@ public class EmojiReaction { public String staticUrl; public transient ImageLoaderRequest request; + public transient boolean pendingChange=false; public String getUrl(boolean playGifs){ String idealUrl=playGifs ? url : staticUrl; @@ -60,4 +61,18 @@ public void add(Account self){ accounts.add(self); accountIds.add(self.id); } + + public EmojiReaction copy() { + EmojiReaction r=new EmojiReaction(); + r.accounts=accounts; + r.accountIds=accountIds; + r.count=count; + r.me=me; + r.name=name; + r.url=url; + r.staticUrl=staticUrl; + r.request=request; + r.pendingChange=pendingChange; + return r; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java index c493c27313..c3d0179134 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -146,14 +146,28 @@ public CatalogInstance toCatalogInstance(){ return ci; } + // This method has almost exclusively been used to improve support for + // Akkoma with no regard for Pleroma, hence its name. However, it is + // more likely than not that most uses should also apply to Pleroma, + // so checking for that too probably causes more good than harm. public boolean isAkkoma() { - return pleroma != null; + return version.contains("compatible; Akkoma") || version.contains("compatible; Pleroma"); } public boolean isPixelfed() { return version.contains("compatible; Pixelfed"); } + // For both Iceshrimp-JS and Iceshrimp.NET + public boolean isIceshrimp() { + return version.contains("compatible; Iceshrimp"); + } + + // Only for Iceshrimp-JS + public boolean isIceshrimpJs() { + return version.contains("compatible; Iceshrimp "); // Iceshrimp.NET will not have a space immediately after + } + public boolean hasFeature(Feature feature) { Optional> pleromaFeatures = Optional.ofNullable(pleroma) .map(p -> p.metadata) @@ -219,6 +233,7 @@ public static class Configuration{ public StatusesConfiguration statuses; public MediaAttachmentsConfiguration mediaAttachments; public PollsConfiguration polls; + public ReactionsConfiguration reactions; } @Parcel @@ -246,6 +261,12 @@ public static class PollsConfiguration{ public int maxExpiration; } + @Parcel + public static class ReactionsConfiguration { + public int maxReactions; + public String defaultReaction; + } + @Parcel public static class V2 extends BaseModel { public V2.Configuration configuration; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java index 3eece84482..f0555f6f0f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java @@ -1,5 +1,6 @@ package org.joinmastodon.android.ui.displayitems; +import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.graphics.Paint; @@ -33,16 +34,24 @@ import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent; +import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.account_list.StatusEmojiReactionsListFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiReaction; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard; import org.joinmastodon.android.ui.utils.TextDrawable; import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.ui.views.ProgressBarButton; +import org.joinmastodon.android.ui.views.EmojiReactionButton; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -62,6 +71,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { private final boolean hideEmpty, forAnnouncement, playGifs; private final String accountID; private static final float ALPHA_DISABLED=0.55f; + private boolean forceShow=false; public EmojiReactionsStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, String accountID, boolean hideEmpty, boolean forAnnouncement) { super(parentID, parentFragment); @@ -90,6 +100,10 @@ public Type getType(){ } public boolean isHidden(){ + if(forceShow){ + forceShow=false; + return false; + } return status.reactions.isEmpty() && hideEmpty; } @@ -101,7 +115,7 @@ private void setActionProgressVisible(Holder.EmojiReactionViewHolder vh, boolean vh.btn.setAlpha(visible ? ALPHA_DISABLED : 1); } - private MastodonAPIRequest createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Runnable cb, Runnable err){ + private MastodonAPIRequest createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Consumer cb, Runnable err){ setActionProgressVisible(vh, true); boolean ak=parentFragment.isInstanceAkkoma(); boolean keepSpinning=delete && count == 1; @@ -113,7 +127,7 @@ private MastodonAPIRequest createRequest(String name, int count, boolean dele @Override public void onSuccess(Object result){ if(!keepSpinning) setActionProgressVisible(vh, false); - cb.run(); + cb.accept(null); } @Override public void onError(ErrorResponse error){ @@ -130,7 +144,7 @@ public void onError(ErrorResponse error){ @Override public void onSuccess(Status result){ if(!keepSpinning) setActionProgressVisible(vh, false); - cb.run(); + cb.accept(result); } @Override public void onError(ErrorResponse error){ @@ -151,6 +165,8 @@ public static class Holder extends StatusDisplayItem.Holderr.me).count(); + boolean canReact=meReactionCountr.request=r.getUrl(item.playGifs)!=null ? new UrlImageLoaderRequest(r.getUrl(item.playGifs), 0, V.sp(24)) : null); @@ -182,18 +205,34 @@ public void onBind(EmojiReactionsStatusDisplayItem item) { emojiKeyboard.setListener(this); space.setVisibility(View.GONE); root.addView(emojiKeyboard.getView()); - boolean hidden=item.isHidden(); - root.setVisibility(hidden ? View.GONE : View.VISIBLE); - line.setVisibility(hidden ? View.GONE : View.VISIBLE); + updateVisibility(item.isHidden(), true); + imgLoader.updateImages(); + adapter.notifyDataSetChanged(); + + if(!GlobalUserPreferences.showDividers || item.isHidden()) + return; + + StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null); + if(next!=null && !next.parentID.equals(item.parentID)) next=null; + if(next instanceof ExtendedFooterStatusDisplayItem) + itemView.setPadding(0, 0, 0, V.dp(12)); + else + itemView.setPadding(0, 0, 0, 0); + } + + private void updateVisibility(boolean hidden, boolean force){ + int visibility=hidden ? View.GONE : View.VISIBLE; + if(!force && visibility==root.getVisibility()) + return; + root.setVisibility(visibility); + line.setVisibility(visibility); line.setPadding( list.getPaddingLeft(), hidden ? 0 : V.dp(8), list.getPaddingRight(), item.forAnnouncement ? V.dp(8) : 0 ); - imgLoader.updateImages(); - adapter.notifyDataSetChanged(); - } + } private void hideEmojiKeyboard(){ space.setVisibility(View.GONE); @@ -244,19 +283,32 @@ private void addEmojiReaction(String emoji, Emoji info) { } } EmojiReaction finalExisting=existing; - item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, ()->{ + item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, (status)->{ resetBtn.run(); if(finalExisting==null){ - int pos=item.status.reactions.size(); + int pos=status.reactions.stream() + .filter(r->r.name.equals(info!=null ? info.shortcode : emoji)) + .findFirst() + .map(r->status.reactions.indexOf(r)) + .orElse(item.status.reactions.size()); + boolean previouslyEmpty=item.status.reactions.isEmpty(); item.status.reactions.add(pos, info!=null ? EmojiReaction.of(info, me) : EmojiReaction.of(emoji, me)); - adapter.notifyItemRangeInserted(pos, 1); + if(previouslyEmpty) + adapter.notifyItemChanged(pos); + else + adapter.notifyItemInserted(pos); RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext()); scroller.setTargetPosition(pos); list.getLayoutManager().startSmoothScroll(scroller); + updateMeReactionCount(false); }else{ finalExisting.add(me); adapter.notifyItemChanged(item.status.reactions.indexOf(finalExisting)); } + if(instance.isIceshrimpJs() && status!=null){ + item.parentFragment.onFavoriteChanged(status, getItemID()); + E.post(new StatusCountersUpdatedEvent(status)); + } E.post(new EmojiReactionsUpdatedEvent(item.status.id, item.status.reactions, countBefore==0, adapter.parentHolder)); }, resetBtn).exec(item.accountID); } @@ -278,6 +330,99 @@ private void onReactClick(View v){ } } + private void updateAddButtonClickable() { + if(instance==null || instance.configuration==null || instance.configuration.reactions==null || instance.configuration.reactions.maxReactions==0) + return; + boolean canReact=meReactionCount reactions){ + item.status.reactions=new ArrayList<>(item.status.reactions); // I don't know how, but this seemingly fixes a bug + + List toRemove=new ArrayList<>(); + for(int i=0;i newReactionOptional=reactions.stream().filter(r->r.name.equals(reaction.name)).findFirst(); + if(newReactionOptional.isEmpty()){ // deleted reactions + toRemove.add(reaction); + continue; + } + + // changed reactions + EmojiReaction newReaction=newReactionOptional.get(); + if(reaction.count!=newReaction.count || reaction.me!=newReaction.me || reaction.pendingChange!=newReaction.pendingChange){ + if(newReaction.pendingChange){ + View holderView=list.getChildAt(i); + if(holderView!=null){ + EmojiReactionViewHolder reactionHolder=(EmojiReactionViewHolder) list.getChildViewHolder(holderView); + item.setActionProgressVisible(reactionHolder, true); + } + }else{ + item.status.reactions.set(i, newReaction); + adapter.notifyItemChanged(i); + } + } + } + + Collections.reverse(toRemove); + for(EmojiReaction r:toRemove){ + int index=item.status.reactions.indexOf(r); + item.status.reactions.remove(index); + adapter.notifyItemRemoved(index); + } + + boolean pendingAddReaction=false; + for(int i=0;ir.name.equals(reaction.name))) + continue; + + // new reactions + if(reaction.pendingChange){ + pendingAddReaction=true; + item.forceShow=true; + continue; + } + boolean previouslyEmpty=item.status.reactions.isEmpty(); + item.status.reactions.add(i, reaction); + if(previouslyEmpty) + adapter.notifyItemChanged(i); + else + adapter.notifyItemInserted(i); + RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext()); + scroller.setTargetPosition(i); + list.getLayoutManager().startSmoothScroll(scroller); + } + if(pendingAddReaction){ + progress.setVisibility(View.VISIBLE); + addButton.setClickable(false); + addButton.setAlpha(ALPHA_DISABLED); + }else{ + progress.setVisibility(View.GONE); + } + + int newMeReactionCount=(int) reactions.stream().filter(r->r.me || r.pendingChange).count(); + if (newMeReactionCount!=meReactionCount){ + meReactionCount=newMeReactionCount; + updateAddButtonClickable(); + } + + updateVisibility(reactions.isEmpty() && item.hideEmpty, false); + } + @Override public void setImage(int index, Drawable image){ View child=list.getChildAt(index); @@ -330,7 +475,7 @@ public ImageLoaderRequest getImageRequest(int position, int image){ } private static class EmojiReactionViewHolder extends BindableViewHolder> implements ImageLoaderViewHolder{ - private final ProgressBarButton btn; + private final EmojiReactionButton btn; private final ProgressBar progress; public EmojiReactionViewHolder(Context context, RecyclerView list){ @@ -356,6 +501,12 @@ public void clearImage(int index){ @Override public void onBind(Pair item){ + if(item.second.pendingChange){ + itemView.setVisibility(View.GONE); + return; + }else{ + itemView.setVisibility(View.VISIBLE); + } item.first.setActionProgressVisible(this, false); EmojiReactionsStatusDisplayItem parent=item.first; EmojiReaction reaction=item.second; @@ -371,10 +522,25 @@ public void onBind(Pair item){ btn.setCompoundDrawablesRelative(item.first.placeholder, null, null, null); } btn.setSelected(reaction.me); + if(parent.parentFragment.isInstanceIceshrimpJs() && reaction.name.contains("@")){ + btn.setEnabled(false); + btn.setClickable(false); + btn.setLongClickable(true); + }else{ + btn.setEnabled(true); + btn.setClickable(true); + } btn.setOnClickListener(e->{ + EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter(); + Instance instance = adapter.parentHolder.instance; + if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0 && + adapter.parentHolder.meReactionCount >= instance.configuration.reactions.maxReactions && + !reaction.me){ + return; + } + boolean deleting=reaction.me; - parent.createRequest(reaction.name, reaction.count, deleting, this, ()->{ - EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter(); + parent.createRequest(reaction.name, reaction.count, deleting, this, (status)->{ for(int i=0; i item){ adapter.parentHolder.root.setVisibility(View.GONE); adapter.parentHolder.line.setVisibility(View.GONE); } + + if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0){ + adapter.parentHolder.updateMeReactionCount(deleting); + } + if(instance.isIceshrimpJs() && status!=null){ + parent.parentFragment.onFavoriteChanged(status, adapter.parentHolder.getItemID()); + E.post(new StatusCountersUpdatedEvent(status)); + } E.post(new EmojiReactionsUpdatedEvent(parent.status.id, parent.status.reactions, parent.status.reactions.isEmpty(), adapter.parentHolder)); adapter.parentHolder.imgLoader.updateImages(); }, null).exec(parent.parentFragment.getAccountID()); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index e4dbdcc7fa..26e995d9b9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -46,7 +46,6 @@ import me.grishka.appkit.utils.V; public class FooterStatusDisplayItem extends StatusDisplayItem{ - public final Status status; private final String accountID; public boolean hideCounts; @@ -316,17 +315,16 @@ private boolean onBoostLongClick(View v){ UiUtils.opacityIn(v); Bundle args=new Bundle(); args.putString("account", item.accountID); - AccountSession accountSession=AccountSessionManager.getInstance().getAccount(item.accountID); - Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); - if(instance.pleroma == null){ + Instance instance=AccountSessionManager.get(item.accountID).getInstance().get(); + if(instance.isAkkoma() || instance.isIceshrimp()){ + args.putParcelable("quote", Parcels.wrap(item.status)); + }else{ StringBuilder prefilledText = new StringBuilder().append("\n\n"); String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id; if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' '); prefilledText.append(item.status.url); args.putString("prefilledText", prefilledText.toString()); args.putInt("selectionStart", 0); - }else{ - args.putParcelable("quote", Parcels.wrap(item.status)); } Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); }); @@ -335,6 +333,20 @@ private boolean onBoostLongClick(View v){ return true; } + public void onFavoriteClick() { + favorite.setSelected(item.status.favourited); + favorite.animate().scaleX(0.95f).scaleY(0.95f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start(); + UiUtils.opacityOut(favorite); + favorite.postDelayed(() -> { + favorite.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start(); + UiUtils.opacityIn(favorite); + if(item.status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) { + favorite.startAnimation(spin); + } + }, 300); + bindText(favorites, item.status.favouritesCount); + } + private void onFavoriteClick(View v){ if(item.status.preview) return; applyInteraction(v, status -> { diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 58b8285e6f..b172839279 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -113,7 +113,7 @@ public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, } public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer consumeReadID) { - HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null); + HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt!=null ? a.startsAt : fakeStatus.createdAt, parentFragment, accountID, fakeStatus, null, null, null); item.announcement = a; item.consumeReadAnnouncement = consumeReadID; return item; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 18d48b8d54..af0d5b95ce 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -274,8 +274,10 @@ public static ArrayList buildItems(BaseStatusListFragment contentItems=items; } - if(statusForContent.quote!=null){ + if(statusForContent.quote!=null) { int quoteInlineIndex=statusForContent.content.lastIndexOf("

RE:"); + if(quoteInlineIndex==-1) + quoteInlineIndex=statusForContent.content.lastIndexOf("

RE:"); if(quoteInlineIndex!=-1) statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex); else { diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/EmojiReactionButton.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/EmojiReactionButton.java new file mode 100644 index 0000000000..9247a368e3 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/EmojiReactionButton.java @@ -0,0 +1,34 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +public class EmojiReactionButton extends ProgressBarButton { + private final Handler handler=new Handler(); + + public EmojiReactionButton(Context context){ + super(context); + } + + public EmojiReactionButton(Context context, AttributeSet attrs){ + super(context, attrs); + } + + public EmojiReactionButton(Context context, AttributeSet attrs, int defStyleAttr){ + super(context, attrs, defStyleAttr); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // allow long click even if button is disabled + int action=event.getAction(); + if(action==MotionEvent.ACTION_DOWN && !isEnabled()) + handler.postDelayed(this::performLongClick, ViewConfiguration.getLongPressTimeout()); + if(action==MotionEvent.ACTION_UP) + handler.removeCallbacksAndMessages(null); + return super.onTouchEvent(event); + } +} diff --git a/mastodon/src/main/res/layout/item_emoji_reaction.xml b/mastodon/src/main/res/layout/item_emoji_reaction.xml index b5e9882c46..e9f131caf2 100644 --- a/mastodon/src/main/res/layout/item_emoji_reaction.xml +++ b/mastodon/src/main/res/layout/item_emoji_reaction.xml @@ -15,7 +15,7 @@ android:indeterminate="true" android:outlineProvider="none" android:visibility="gone"/> -