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 99b0beb48..f7610292b 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 @@ import okhttp3.Response; 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 abstract class MastodonAPIRequest extends APIRequest{ 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 0526ab79b..e7f89590d 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.SetStatusBookmarked; 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 class StatusInteractionController{ 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 class StatusInteractionController{ 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 class StatusInteractionController{ 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 e17136d9e..0db74f3d0 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.ContentType; 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.ArrayList; 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 class AccountLocalPreferences{ // 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 e09649a62..1bb7a70ce 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 @@ -71,7 +71,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; @@ -110,7 +109,7 @@ public class AccountSessionManager{ Log.e(TAG, "Error loading accounts", x); } lastActiveAccountID=prefs.getString("lastActiveAccount", null); - MastodonAPIController.runInBackground(()->readInstanceInfo(domains)); + readInstanceInfo(domains); maybeUpdateShortcuts(); } @@ -248,11 +247,6 @@ public class AccountSessionManager{ 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 b376f24dc..e267b45b1 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 @@ public class AnnouncementsFragment extends BaseStatusListFragment 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 810ad2aba..e9f66df60 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 abstract class BaseStatusListFragment exten 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 589edeb08..6032b2867 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 class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } 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 9934b2e17..d8f84e565 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 @@ public interface HasAccountID { 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 1c6cbbf6d..92d2f7143 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.EmojiReactionsUpdatedEvent; 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 @@ public class NotificationsListFragment extends BaseStatusListFragment public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){ for(Status s:data){ if(s.getContentStatus().id.equals(ev.id)){ - s.getContentStatus().update(ev); - AccountSessionManager.get(accountID).getCacheController().updateStatus(s); 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 class DiscoverFragment extends AppKitFragment implements ScrollableToTop, 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 class DiscoverFragment extends AppKitFragment implements ScrollableToTop, 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 class DiscoverFragment extends AppKitFragment implements ScrollableToTop, } }); - 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 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, 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 4cf293904..f091830aa 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 class SettingsBehaviorFragment extends BaseSettingsFragment impleme 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 class SettingsBehaviorFragment extends BaseSettingsFragment impleme 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 6dd90718a..35dbd6286 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.model.viewmodel.ListItem; 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 class SettingsInstanceFragment extends BaseSettingsFragment impleme 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 @@ public class SettingsInstanceFragment extends BaseSettingsFragment impleme @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 @@ public class SettingsInstanceFragment extends BaseSettingsFragment impleme 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 20142ebbd..ebfef8542 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 class SettingsMainFragment extends BaseSettingsFragment{ )); 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 9c769ed03..ba8a949ed 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 class Announcement extends BaseModel implements DisplayItemsParent { 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 79957bfcb..24f1d4161 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 enum ContentType { } 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 4ffed6e6d..12ec162c8 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 class EmojiReaction { 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 f4d315988..6c408418c 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 class Instance extends BaseModel{ 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 class Instance extends BaseModel{ public StatusesConfiguration statuses; public MediaAttachmentsConfiguration mediaAttachments; public PollsConfiguration polls; + public ReactionsConfiguration reactions; } @Parcel @@ -246,6 +261,12 @@ public class Instance extends BaseModel{ public long 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 3eece8448..f0555f6f0 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.requests.statuses.PleromaDeleteStatusReactio 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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { } public boolean isHidden(){ + if(forceShow){ + forceShow=false; + return false; + } return status.reactions.isEmpty() && hideEmpty; } @@ -101,7 +115,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { 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 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { @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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { @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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { private final ProgressBar progress; private final EmojiReactionsAdapter adapter; private final ListImageLoaderWrapper imgLoader; + private int meReactionCount=0; + private Instance instance; public Holder(Activity activity, ViewGroup parent) { super(activity, R.layout.display_item_emoji_reactions, parent); @@ -171,6 +187,13 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { if(emojiKeyboard != null) root.removeView(emojiKeyboard.getView()); addButton.setSelected(false); AccountSession session=item.parentFragment.getSession(); + instance=item.parentFragment.getInstance().get(); + if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0){ + meReactionCount=(int) item.status.reactions.stream().filter(r->r.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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { 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 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { } } 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 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { } } + 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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { } 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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { @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 class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { 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 { + 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 cf93a9cf4..98e4d0e05 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 class HeaderStatusDisplayItem extends StatusDisplayItem{ } 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 18d48b8d5..af0d5b95c 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 abstract class StatusDisplayItem{ 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 000000000..9247a368e --- /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 b5e9882c4..e9f131caf 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"/> -