diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AddStatusReaction.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AddStatusReaction.java new file mode 100644 index 000000000..536634655 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/AddStatusReaction.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class AddStatusReaction extends MastodonAPIRequest { + public AddStatusReaction(String id, String emoji) { + super(HttpMethod.POST, "/statuses/" + id + "/react/" + emoji, Status.class); + setRequestBody(new Object()); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/DeleteStatusReaction.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/DeleteStatusReaction.java new file mode 100644 index 000000000..133b09730 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/DeleteStatusReaction.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class DeleteStatusReaction extends MastodonAPIRequest { + public DeleteStatusReaction(String id, String emoji) { + super(HttpMethod.POST, "/statuses/" + id + "/unreact/" + emoji, Status.class); + setRequestBody(new Object()); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaAddStatusReaction.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaAddStatusReaction.java new file mode 100644 index 000000000..fcf25cfb7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaAddStatusReaction.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class PleromaAddStatusReaction extends MastodonAPIRequest { + public PleromaAddStatusReaction(String id, String emoji) { + super(HttpMethod.PUT, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class); + setRequestBody(new Object()); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaDeleteStatusReaction.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaDeleteStatusReaction.java new file mode 100644 index 000000000..5657c23df --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaDeleteStatusReaction.java @@ -0,0 +1,10 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class PleromaDeleteStatusReaction extends MastodonAPIRequest { + public PleromaDeleteStatusReaction(String id, String emoji) { + super(HttpMethod.DELETE, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaGetStatusReactions.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaGetStatusReactions.java new file mode 100644 index 000000000..e344321db --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/PleromaGetStatusReactions.java @@ -0,0 +1,14 @@ +package org.joinmastodon.android.api.requests.statuses; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.EmojiReaction; + +import java.util.List; + +public class PleromaGetStatusReactions extends MastodonAPIRequest> { + public PleromaGetStatusReactions(String id, String emoji) { + super(HttpMethod.GET, "/pleroma/statuses/" + id + "/reactions/" + (emoji != null ? emoji : ""), new TypeToken<>(){}); + } +} 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 7da87e5a8..4589ec4ed 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 @@ -38,6 +38,9 @@ public class AccountLocalPreferences{ public String timelineReplyVisibility; // akkoma-only public boolean keepOnlyLatestNotification; + public boolean emojiReactionsEnabled; + public boolean showEmojiReactionsInLists; + private final static Type recentLanguagesType = new TypeToken>() {}.getType(); private final static Type timelinesType = new TypeToken>() {}.getType(); @@ -62,6 +65,8 @@ public class AccountLocalPreferences{ publishButtonText=prefs.getString("publishButtonText", null); timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null); keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false); + emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma()); + showEmojiReactionsInLists=prefs.getBoolean("showEmojiReactionsInLists", false); } public long getNotificationsPauseEndTime(){ @@ -93,6 +98,8 @@ public class AccountLocalPreferences{ .putString("publishButtonText", publishButtonText) .putString("timelineReplyVisibility", timelineReplyVisibility) .putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification) + .putBoolean("emojiReactionsEnabled", emojiReactionsEnabled) + .putBoolean("showEmojiReactionsInLists", showEmojiReactionsInLists) .apply(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java index e87e0fa20..3027a3809 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java +++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java @@ -1,15 +1,27 @@ package org.joinmastodon.android.events; +import androidx.recyclerview.widget.RecyclerView; + import org.joinmastodon.android.api.CacheController; +import org.joinmastodon.android.model.EmojiReaction; import org.joinmastodon.android.model.Status; +import java.util.ArrayList; +import java.util.List; + public class StatusCountersUpdatedEvent{ public String id; public long favorites, reblogs, replies; public boolean favorited, reblogged, bookmarked, pinned; + public List reactions; public Status status; + public RecyclerView.ViewHolder viewHolder; public StatusCountersUpdatedEvent(Status s){ + this(s, null); + } + + public StatusCountersUpdatedEvent(Status s, RecyclerView.ViewHolder vh){ id=s.id; status=s; favorites=s.favouritesCount; @@ -19,5 +31,7 @@ public class StatusCountersUpdatedEvent{ reblogged=s.reblogged; bookmarked=s.bookmarked; pinned=s.pinned; + reactions=new ArrayList<>(s.reactions); + viewHolder=vh; } } 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 25dc36138..0cb77d37d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -31,6 +31,7 @@ import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; @@ -607,6 +608,15 @@ public abstract class BaseStatusListFragment exten if (header != null) header.rebind(); } + public void updateEmojiReactions(Status status, String itemID){ + EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class); + if(reactions != null){ + reactions.getItem().status.reactions.clear(); + reactions.getItem().status.reactions.addAll(status.reactions); + reactions.rebind(); + } + } + public void onGapClick(GapStatusDisplayItem.Holder item){} public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){ @@ -782,6 +792,10 @@ public abstract class BaseStatusListFragment exten } } + public void scrollBy(int x, int y) { + list.scrollBy(x, y); + } + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ 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 35a3021d7..bd88e6203 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -300,6 +300,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr onCustomEmojiClick(emoji); } + @Override + public void onEmojiSelected(String emoji){ + if(getActivity().getCurrentFocus() instanceof EditText edit && edit == mainEditText){ + edit.getText().replace(edit.getSelectionStart(), edit.getSelectionEnd(), emoji); + } + } + @Override public void onBackspace(){ getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); 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 697f65f3f..33cba9f52 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -12,8 +12,8 @@ import android.view.View; import com.squareup.otto.Subscribe; import org.joinmastodon.android.E; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent; @@ -22,6 +22,7 @@ import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem; @@ -99,7 +100,11 @@ public class NotificationsListFragment extends BaseStatusListFragment items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags); if(titleItem!=null) items.add(0, titleItem); @@ -254,6 +259,8 @@ public class NotificationsListFragment extends BaseStatusListFragment buildDisplayItems(ScheduledStatus s) { - return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, true, null); + return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, false, true, null); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java index 2f2c57b1a..26ffd1260 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java @@ -57,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{ @Override protected List buildDisplayItems(Status s){ - List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); + List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); int idx=data.indexOf(s); if(idx>=0){ String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); @@ -143,9 +143,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{ } } String sep = getString(R.string.sk_separator); - ReblogOrReplyLineStatusDisplayItem line=new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null); - line.needBottomPadding=true; - items.add(0, line); + items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null)); } return items; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index 9e348f884..14718da16 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -16,6 +16,7 @@ import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -33,9 +34,13 @@ public abstract class StatusListFragment extends BaseStatusListFragment protected EventListener eventListener=new EventListener(); protected List buildDisplayItems(Status s){ - boolean addFooter = !GlobalUserPreferences.spectatorMode || - (this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id)); - return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), addFooter ? 0 : StatusDisplayItem.FLAG_NO_FOOTER); + boolean isMainThreadStatus = this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id); + int flags = 0; + if (GlobalUserPreferences.spectatorMode) + flags |= StatusDisplayItem.FLAG_NO_FOOTER; + if (!getLocalPrefs().showEmojiReactionsInLists) + flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS; + return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags); } protected abstract FilterContext getFilterContext(); @@ -218,6 +223,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment footer.rebind(); }else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){ footer.rebind(); + }else if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && ev.viewHolder!=holder){ + reactions.rebind(); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusEmojiReactionsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusEmojiReactionsListFragment.java new file mode 100644 index 000000000..665d70fea --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusEmojiReactionsListFragment.java @@ -0,0 +1,97 @@ +package org.joinmastodon.android.fragments.account_list; + +import android.net.Uri; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.view.View; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.statuses.PleromaGetStatusReactions; +import org.joinmastodon.android.model.Emoji; +import org.joinmastodon.android.model.EmojiReaction; +import org.joinmastodon.android.model.viewmodel.AccountViewModel; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import me.grishka.appkit.api.ErrorResponse; +import me.grishka.appkit.api.SimpleCallback; + +public class StatusEmojiReactionsListFragment extends BaseAccountListFragment { + private String id; + private String emojiName; + private String url; + private int count; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + id = getArguments().getString("statusID"); + emojiName = getArguments().getString("emoji"); + url = getArguments().getString("url"); + count = getArguments().getInt("count"); + + SpannableStringBuilder title = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.sk_users_reacted_with, count, + count, url == null ? emojiName : ":"+emojiName+":")); + if (url != null) { + Emoji emoji = new Emoji(); + emoji.shortcode = emojiName; + emoji.url = url; + HtmlParser.parseCustomEmoji(title, Collections.singletonList(emoji)); + } + setTitle(title); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (url != null) { + UiUtils.loadCustomEmojiInTextView(toolbarTitleView); + } + } + + @Override + public void dataLoaded() { + super.dataLoaded(); + footerProgress.setVisibility(View.GONE); + } + + @Override + protected void doLoadData(int offset, int count){ + currentRequest = new PleromaGetStatusReactions(id, emojiName) + .setCallback(new SimpleCallback<>(StatusEmojiReactionsListFragment.this){ + @Override + public void onSuccess(List result) { + if (getActivity() == null) + return; + + List items = result.get(0).accounts.stream() + .map(a -> new AccountViewModel(a, accountID)) + .collect(Collectors.toList()); + + onDataLoaded(items); + } + + @Override + public void onError(ErrorResponse error) { + super.onError(error); + } + }) + .exec(accountID); + } + + @Override + public void onResume(){ + super.onResume(); + if(!loaded && !dataLoading) + loadData(); + } + + @Override + public Uri getWebUri(Uri.Builder base) { + return null; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java index fc7644649..2c0c91859 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.view.View; import android.view.inputmethod.InputMethodManager; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.search.GetSearchResults; import org.joinmastodon.android.api.session.AccountSessionManager; @@ -69,7 +70,7 @@ public class SearchFragment extends BaseStatusListFragment{ return switch(s.type){ case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); - case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, FilterContext.PUBLIC, 0); + case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, FilterContext.PUBLIC, !getLocalPrefs().showEmojiReactionsInLists ? StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS : 0); }; } 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 b747372c0..6d6bdd5d2 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 @@ -19,7 +19,7 @@ import java.util.List; import me.grishka.appkit.Nav; public class SettingsInstanceFragment extends BaseSettingsFragment implements HasAccountID{ - private CheckableListItem contentTypesItem, localOnlyItem, glitchModeItem; + private CheckableListItem contentTypesItem, emojiReactionsItem, emojiReactionsInListsItem, localOnlyItem, glitchModeItem; private ListItem defaultContentTypeItem; private AccountLocalPreferences lp; @@ -36,11 +36,15 @@ public class SettingsInstanceFragment extends BaseSettingsFragment impleme new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, ()->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, this::onContentTypeClick), defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick), + 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, this::onEmojiReactionsClick), + emojiReactionsInListsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions_in_lists, R.string.sk_settings_emoji_reactions_in_lists_explanation, CheckableListItem.Style.SWITCH, lp.showEmojiReactionsInLists, R.drawable.ic_fluent_emoji_24_regular, ()->toggleCheckableItem(emojiReactionsInListsItem)), 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, this::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, ()->toggleCheckableItem(glitchModeItem)) )); contentTypesItem.checkedChangeListener=checked->onContentTypeClick(); defaultContentTypeItem.isEnabled=contentTypesItem.checked; + emojiReactionsItem.checkedChangeListener=checked->onEmojiReactionsClick(); + emojiReactionsInListsItem.isEnabled=emojiReactionsItem.checked; localOnlyItem.checkedChangeListener=checked->onLocalOnlyClick(); glitchModeItem.isEnabled=localOnlyItem.checked; } @@ -52,6 +56,8 @@ public class SettingsInstanceFragment extends BaseSettingsFragment impleme protected void onHidden(){ super.onHidden(); lp.contentTypesEnabled=contentTypesItem.checked; + lp.emojiReactionsEnabled=emojiReactionsItem.checked; + lp.showEmojiReactionsInLists=emojiReactionsInListsItem.checked; lp.localOnlySupported=localOnlyItem.checked; lp.glitchInstance=glitchModeItem.checked; lp.save(); @@ -101,6 +107,13 @@ public class SettingsInstanceFragment extends BaseSettingsFragment impleme .show(); } + private void onEmojiReactionsClick(){ + toggleCheckableItem(emojiReactionsItem); + emojiReactionsInListsItem.checked=false; + emojiReactionsInListsItem.isEnabled=emojiReactionsItem.checked; + rebindItem(emojiReactionsInListsItem); + } + private void onLocalOnlyClick(){ toggleCheckableItem(localOnlyItem); glitchModeItem.checked=localOnlyItem.checked && !isInstanceAkkoma(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java b/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java new file mode 100644 index 000000000..ed73d31a8 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/EmojiReaction.java @@ -0,0 +1,16 @@ +package org.joinmastodon.android.model; + +import org.parceler.Parcel; + +import java.util.List; + +@Parcel +public class EmojiReaction { + public List accounts; + public List accountIds; + public int count; + public boolean me; + public String name; + public String url; + public String staticUrl; +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java index 21bcf9167..6864e7d5f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -23,6 +23,7 @@ import org.parceler.Parcel; import java.lang.reflect.Type; import java.time.Instant; +import java.util.ArrayList; import java.util.List; @Parcel @@ -74,6 +75,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ public Status quote; // can be boolean in calckey + public List reactions; + protected List emojiReactions; // akkoma + public transient boolean filterRevealed; public transient boolean spoilerRevealed; public transient boolean sensitiveRevealed; @@ -110,10 +114,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ for(FilterResult fr:filtered) fr.postprocess(); - if (!TextUtils.isEmpty(spoilerText)) sensitive = true; + if(!TextUtils.isEmpty(spoilerText)) sensitive=true; spoilerRevealed=TextUtils.isEmpty(spoilerText); sensitiveRevealed=!sensitive; - if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true; + if(visibility.equals(StatusPrivacy.LOCAL)) localOnly=true; + if(emojiReactions!=null) reactions=emojiReactions; + if(reactions==null) reactions=new ArrayList<>(); } @Override @@ -169,6 +175,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ reblogged=ev.reblogged; bookmarked=ev.bookmarked; pinned=ev.pinned; + reactions.clear(); + reactions.addAll(ev.reactions); } public Status getContentStatus(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/CustomEmojiPopupKeyboard.java b/mastodon/src/main/java/org/joinmastodon/android/ui/CustomEmojiPopupKeyboard.java index 74f9ef246..eefbe0ba0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/CustomEmojiPopupKeyboard.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/CustomEmojiPopupKeyboard.java @@ -2,20 +2,27 @@ package org.joinmastodon.android.ui; import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import com.squareup.otto.Subscribe; @@ -27,6 +34,7 @@ import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.ui.utils.UiUtils; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; import androidx.annotation.NonNull; @@ -51,11 +59,19 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{ private String domain; private int spanCount=6; private Listener listener; + private boolean forReaction; + // Generated using https://github.com/mathiasbynens/emoji-test-regex-pattern + private static Pattern emojiRegex = Pattern.compile("[#*0-9]\\x{FE0F}?\\x{20E3}|[\\xA9\\xAE\\x{203C}\\x{2049}\\x{2122}\\x{2139}\\x{2194}-\\x{2199}\\x{21A9}\\x{21AA}\\x{231A}\\x{231B}\\x{2328}\\x{23CF}\\x{23ED}-\\x{23EF}\\x{23F1}\\x{23F2}\\x{23F8}-\\x{23FA}\\x{24C2}\\x{25AA}\\x{25AB}\\x{25B6}\\x{25C0}\\x{25FB}\\x{25FC}\\x{25FE}\\x{2600}-\\x{2604}\\x{260E}\\x{2611}\\x{2614}\\x{2615}\\x{2618}\\x{2620}\\x{2622}\\x{2623}\\x{2626}\\x{262A}\\x{262E}\\x{262F}\\x{2638}-\\x{263A}\\x{2640}\\x{2642}\\x{2648}-\\x{2653}\\x{265F}\\x{2660}\\x{2663}\\x{2665}\\x{2666}\\x{2668}\\x{267B}\\x{267E}\\x{267F}\\x{2692}\\x{2694}-\\x{2697}\\x{2699}\\x{269B}\\x{269C}\\x{26A0}\\x{26A7}\\x{26AA}\\x{26B0}\\x{26B1}\\x{26BD}\\x{26BE}\\x{26C4}\\x{26C8}\\x{26CF}\\x{26D1}\\x{26E9}\\x{26F0}-\\x{26F5}\\x{26F7}\\x{26F8}\\x{26FA}\\x{2702}\\x{2708}\\x{2709}\\x{270F}\\x{2712}\\x{2714}\\x{2716}\\x{271D}\\x{2721}\\x{2733}\\x{2734}\\x{2744}\\x{2747}\\x{2757}\\x{2763}\\x{27A1}\\x{2934}\\x{2935}\\x{2B05}-\\x{2B07}\\x{2B1B}\\x{2B1C}\\x{2B55}\\x{3030}\\x{303D}\\x{3297}\\x{3299}\\x{1F004}\\x{1F170}\\x{1F171}\\x{1F17E}\\x{1F17F}\\x{1F202}\\x{1F237}\\x{1F321}\\x{1F324}-\\x{1F32C}\\x{1F336}\\x{1F37D}\\x{1F396}\\x{1F397}\\x{1F399}-\\x{1F39B}\\x{1F39E}\\x{1F39F}\\x{1F3CD}\\x{1F3CE}\\x{1F3D4}-\\x{1F3DF}\\x{1F3F5}\\x{1F3F7}\\x{1F43F}\\x{1F4FD}\\x{1F549}\\x{1F54A}\\x{1F56F}\\x{1F570}\\x{1F573}\\x{1F576}-\\x{1F579}\\x{1F587}\\x{1F58A}-\\x{1F58D}\\x{1F5A5}\\x{1F5A8}\\x{1F5B1}\\x{1F5B2}\\x{1F5BC}\\x{1F5C2}-\\x{1F5C4}\\x{1F5D1}-\\x{1F5D3}\\x{1F5DC}-\\x{1F5DE}\\x{1F5E1}\\x{1F5E3}\\x{1F5E8}\\x{1F5EF}\\x{1F5F3}\\x{1F5FA}\\x{1F6CB}\\x{1F6CD}-\\x{1F6CF}\\x{1F6E0}-\\x{1F6E5}\\x{1F6E9}\\x{1F6F0}\\x{1F6F3}]\\x{FE0F}?|[\\x{261D}\\x{270C}\\x{270D}\\x{1F574}\\x{1F590}][\\x{FE0F}\\x{1F3FB}-\\x{1F3FF}]?|[\\x{26F9}\\x{1F3CB}\\x{1F3CC}\\x{1F575}][\\x{FE0F}\\x{1F3FB}-\\x{1F3FF}]?(?:\\x{200D}[\\x{2640}\\x{2642}]\\x{FE0F}?)?|[\\x{270A}\\x{270B}\\x{1F385}\\x{1F3C2}\\x{1F3C7}\\x{1F442}\\x{1F443}\\x{1F446}-\\x{1F450}\\x{1F466}\\x{1F467}\\x{1F46B}-\\x{1F46D}\\x{1F472}\\x{1F474}-\\x{1F476}\\x{1F478}\\x{1F47C}\\x{1F483}\\x{1F485}\\x{1F48F}\\x{1F491}\\x{1F4AA}\\x{1F57A}\\x{1F595}\\x{1F596}\\x{1F64C}\\x{1F64F}\\x{1F6C0}\\x{1F6CC}\\x{1F90C}\\x{1F90F}\\x{1F918}-\\x{1F91F}\\x{1F930}-\\x{1F934}\\x{1F936}\\x{1F977}\\x{1F9B5}\\x{1F9B6}\\x{1F9BB}\\x{1F9D2}\\x{1F9D3}\\x{1F9D5}\\x{1FAC3}-\\x{1FAC5}\\x{1FAF0}\\x{1FAF2}-\\x{1FAF8}][\\x{1F3FB}-\\x{1F3FF}]?|[\\x{1F3C3}\\x{1F6B6}\\x{1F9CE}][\\x{1F3FB}-\\x{1F3FF}]?(?:\\x{200D}(?:[\\x{2640}\\x{2642}]\\x{FE0F}?(?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|\\x{27A1}\\x{FE0F}?))?|[\\x{1F3C4}\\x{1F3CA}\\x{1F46E}\\x{1F470}\\x{1F471}\\x{1F473}\\x{1F477}\\x{1F481}\\x{1F482}\\x{1F486}\\x{1F487}\\x{1F645}-\\x{1F647}\\x{1F64B}\\x{1F64D}\\x{1F64E}\\x{1F6A3}\\x{1F6B4}\\x{1F6B5}\\x{1F926}\\x{1F935}\\x{1F937}-\\x{1F939}\\x{1F93D}\\x{1F93E}\\x{1F9B8}\\x{1F9B9}\\x{1F9CD}\\x{1F9CF}\\x{1F9D4}\\x{1F9D6}-\\x{1F9DD}][\\x{1F3FB}-\\x{1F3FF}]?(?:\\x{200D}[\\x{2640}\\x{2642}]\\x{FE0F}?)?|[\\x{1F46F}\\x{1F9DE}\\x{1F9DF}](?:\\x{200D}[\\x{2640}\\x{2642}]\\x{FE0F}?)?|[\\x{23E9}-\\x{23EC}\\x{23F0}\\x{23F3}\\x{25FD}\\x{2693}\\x{26A1}\\x{26AB}\\x{26C5}\\x{26CE}\\x{26D4}\\x{26EA}\\x{26FD}\\x{2705}\\x{2728}\\x{274C}\\x{274E}\\x{2753}-\\x{2755}\\x{2795}-\\x{2797}\\x{27B0}\\x{27BF}\\x{2B50}\\x{1F0CF}\\x{1F18E}\\x{1F191}-\\x{1F19A}\\x{1F201}\\x{1F21A}\\x{1F22F}\\x{1F232}-\\x{1F236}\\x{1F238}-\\x{1F23A}\\x{1F250}\\x{1F251}\\x{1F300}-\\x{1F320}\\x{1F32D}-\\x{1F335}\\x{1F337}-\\x{1F343}\\x{1F345}-\\x{1F34A}\\x{1F34C}-\\x{1F37C}\\x{1F37E}-\\x{1F384}\\x{1F386}-\\x{1F393}\\x{1F3A0}-\\x{1F3C1}\\x{1F3C5}\\x{1F3C6}\\x{1F3C8}\\x{1F3C9}\\x{1F3CF}-\\x{1F3D3}\\x{1F3E0}-\\x{1F3F0}\\x{1F3F8}-\\x{1F407}\\x{1F409}-\\x{1F414}\\x{1F416}-\\x{1F425}\\x{1F427}-\\x{1F43A}\\x{1F43C}-\\x{1F43E}\\x{1F440}\\x{1F444}\\x{1F445}\\x{1F451}-\\x{1F465}\\x{1F46A}\\x{1F479}-\\x{1F47B}\\x{1F47D}-\\x{1F480}\\x{1F484}\\x{1F488}-\\x{1F48E}\\x{1F490}\\x{1F492}-\\x{1F4A9}\\x{1F4AB}-\\x{1F4FC}\\x{1F4FF}-\\x{1F53D}\\x{1F54B}-\\x{1F54E}\\x{1F550}-\\x{1F567}\\x{1F5A4}\\x{1F5FB}-\\x{1F62D}\\x{1F62F}-\\x{1F634}\\x{1F637}-\\x{1F641}\\x{1F643}\\x{1F644}\\x{1F648}-\\x{1F64A}\\x{1F680}-\\x{1F6A2}\\x{1F6A4}-\\x{1F6B3}\\x{1F6B7}-\\x{1F6BF}\\x{1F6C1}-\\x{1F6C5}\\x{1F6D0}-\\x{1F6D2}\\x{1F6D5}-\\x{1F6D7}\\x{1F6DC}-\\x{1F6DF}\\x{1F6EB}\\x{1F6EC}\\x{1F6F4}-\\x{1F6FC}\\x{1F7E0}-\\x{1F7EB}\\x{1F7F0}\\x{1F90D}\\x{1F90E}\\x{1F910}-\\x{1F917}\\x{1F920}-\\x{1F925}\\x{1F927}-\\x{1F92F}\\x{1F93A}\\x{1F93F}-\\x{1F945}\\x{1F947}-\\x{1F976}\\x{1F978}-\\x{1F9B4}\\x{1F9B7}\\x{1F9BA}\\x{1F9BC}-\\x{1F9CC}\\x{1F9D0}\\x{1F9E0}-\\x{1F9FF}\\x{1FA70}-\\x{1FA7C}\\x{1FA80}-\\x{1FA88}\\x{1FA90}-\\x{1FABD}\\x{1FABF}-\\x{1FAC2}\\x{1FACE}-\\x{1FADB}\\x{1FAE0}-\\x{1FAE8}]|\\x{26D3}\\x{FE0F}?(?:\\x{200D}\\x{1F4A5})?|\\x{2764}\\x{FE0F}?(?:\\x{200D}[\\x{1F525}\\x{1FA79}])?|\\x{1F1E6}[\\x{1F1E8}-\\x{1F1EC}\\x{1F1EE}\\x{1F1F1}\\x{1F1F2}\\x{1F1F4}\\x{1F1F6}-\\x{1F1FA}\\x{1F1FC}\\x{1F1FD}\\x{1F1FF}]|\\x{1F1E7}[\\x{1F1E6}\\x{1F1E7}\\x{1F1E9}-\\x{1F1EF}\\x{1F1F1}-\\x{1F1F4}\\x{1F1F6}-\\x{1F1F9}\\x{1F1FB}\\x{1F1FC}\\x{1F1FE}\\x{1F1FF}]|\\x{1F1E8}[\\x{1F1E6}\\x{1F1E8}\\x{1F1E9}\\x{1F1EB}-\\x{1F1EE}\\x{1F1F0}-\\x{1F1F5}\\x{1F1F7}\\x{1F1FA}-\\x{1F1FF}]|\\x{1F1E9}[\\x{1F1EA}\\x{1F1EC}\\x{1F1EF}\\x{1F1F0}\\x{1F1F2}\\x{1F1F4}\\x{1F1FF}]|\\x{1F1EA}[\\x{1F1E6}\\x{1F1E8}\\x{1F1EA}\\x{1F1EC}\\x{1F1ED}\\x{1F1F7}-\\x{1F1FA}]|\\x{1F1EB}[\\x{1F1EE}-\\x{1F1F0}\\x{1F1F2}\\x{1F1F4}\\x{1F1F7}]|\\x{1F1EC}[\\x{1F1E6}\\x{1F1E7}\\x{1F1E9}-\\x{1F1EE}\\x{1F1F1}-\\x{1F1F3}\\x{1F1F5}-\\x{1F1FA}\\x{1F1FC}\\x{1F1FE}]|\\x{1F1ED}[\\x{1F1F0}\\x{1F1F2}\\x{1F1F3}\\x{1F1F7}\\x{1F1F9}\\x{1F1FA}]|\\x{1F1EE}[\\x{1F1E8}-\\x{1F1EA}\\x{1F1F1}-\\x{1F1F4}\\x{1F1F6}-\\x{1F1F9}]|\\x{1F1EF}[\\x{1F1EA}\\x{1F1F2}\\x{1F1F4}\\x{1F1F5}]|\\x{1F1F0}[\\x{1F1EA}\\x{1F1EC}-\\x{1F1EE}\\x{1F1F2}\\x{1F1F3}\\x{1F1F5}\\x{1F1F7}\\x{1F1FC}\\x{1F1FE}\\x{1F1FF}]|\\x{1F1F1}[\\x{1F1E6}-\\x{1F1E8}\\x{1F1EE}\\x{1F1F0}\\x{1F1F7}-\\x{1F1FB}\\x{1F1FE}]|\\x{1F1F2}[\\x{1F1E6}\\x{1F1E8}-\\x{1F1ED}\\x{1F1F0}-\\x{1F1FF}]|\\x{1F1F3}[\\x{1F1E6}\\x{1F1E8}\\x{1F1EA}-\\x{1F1EC}\\x{1F1EE}\\x{1F1F1}\\x{1F1F4}\\x{1F1F5}\\x{1F1F7}\\x{1F1FA}\\x{1F1FF}]|\\x{1F1F4}\\x{1F1F2}|\\x{1F1F5}[\\x{1F1E6}\\x{1F1EA}-\\x{1F1ED}\\x{1F1F0}-\\x{1F1F3}\\x{1F1F7}-\\x{1F1F9}\\x{1F1FC}\\x{1F1FE}]|\\x{1F1F6}\\x{1F1E6}|\\x{1F1F7}[\\x{1F1EA}\\x{1F1F4}\\x{1F1F8}\\x{1F1FA}\\x{1F1FC}]|\\x{1F1F8}[\\x{1F1E6}-\\x{1F1EA}\\x{1F1EC}-\\x{1F1F4}\\x{1F1F7}-\\x{1F1F9}\\x{1F1FB}\\x{1F1FD}-\\x{1F1FF}]|\\x{1F1F9}[\\x{1F1E6}\\x{1F1E8}\\x{1F1E9}\\x{1F1EB}-\\x{1F1ED}\\x{1F1EF}-\\x{1F1F4}\\x{1F1F7}\\x{1F1F9}\\x{1F1FB}\\x{1F1FC}\\x{1F1FF}]|\\x{1F1FA}[\\x{1F1E6}\\x{1F1EC}\\x{1F1F2}\\x{1F1F3}\\x{1F1F8}\\x{1F1FE}\\x{1F1FF}]|\\x{1F1FB}[\\x{1F1E6}\\x{1F1E8}\\x{1F1EA}\\x{1F1EC}\\x{1F1EE}\\x{1F1F3}\\x{1F1FA}]|\\x{1F1FC}[\\x{1F1EB}\\x{1F1F8}]|\\x{1F1FD}\\x{1F1F0}|\\x{1F1FE}[\\x{1F1EA}\\x{1F1F9}]|\\x{1F1FF}[\\x{1F1E6}\\x{1F1F2}\\x{1F1FC}]|\\x{1F344}(?:\\x{200D}\\x{1F7EB})?|\\x{1F34B}(?:\\x{200D}\\x{1F7E9})?|\\x{1F3F3}\\x{FE0F}?(?:\\x{200D}(?:\\x{26A7}\\x{FE0F}?|\\x{1F308}))?|\\x{1F3F4}(?:\\x{200D}\\x{2620}\\x{FE0F}?|\\x{E0067}\\x{E0062}(?:\\x{E0065}\\x{E006E}\\x{E0067}|\\x{E0073}\\x{E0063}\\x{E0074}|\\x{E0077}\\x{E006C}\\x{E0073})\\x{E007F})?|\\x{1F408}(?:\\x{200D}\\x{2B1B})?|\\x{1F415}(?:\\x{200D}\\x{1F9BA})?|\\x{1F426}(?:\\x{200D}[\\x{2B1B}\\x{1F525}])?|\\x{1F43B}(?:\\x{200D}\\x{2744}\\x{FE0F}?)?|\\x{1F441}\\x{FE0F}?(?:\\x{200D}\\x{1F5E8}\\x{FE0F}?)?|\\x{1F468}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F468}\\x{1F469}]\\x{200D}(?:\\x{1F466}(?:\\x{200D}\\x{1F466})?|\\x{1F467}(?:\\x{200D}[\\x{1F466}\\x{1F467}])?)|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}|\\x{1F466}(?:\\x{200D}\\x{1F466})?|\\x{1F467}(?:\\x{200D}[\\x{1F466}\\x{1F467}])?)|\\x{1F3FB}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F468}[\\x{1F3FC}-\\x{1F3FF}]))?|\\x{1F3FC}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F468}[\\x{1F3FB}\\x{1F3FD}-\\x{1F3FF}]))?|\\x{1F3FD}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F468}[\\x{1F3FB}\\x{1F3FC}\\x{1F3FE}\\x{1F3FF}]))?|\\x{1F3FE}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F468}[\\x{1F3FB}-\\x{1F3FD}\\x{1F3FF}]))?|\\x{1F3FF}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F468}[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F468}[\\x{1F3FB}-\\x{1F3FE}]))?)?|\\x{1F469}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?[\\x{1F468}\\x{1F469}]|\\x{1F466}(?:\\x{200D}\\x{1F466})?|\\x{1F467}(?:\\x{200D}[\\x{1F466}\\x{1F467}])?|\\x{1F469}\\x{200D}(?:\\x{1F466}(?:\\x{200D}\\x{1F466})?|\\x{1F467}(?:\\x{200D}[\\x{1F466}\\x{1F467}])?))|\\x{1F3FB}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:[\\x{1F468}\\x{1F469}]|\\x{1F48B}\\x{200D}[\\x{1F468}\\x{1F469}])[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}[\\x{1F468}\\x{1F469}][\\x{1F3FC}-\\x{1F3FF}]))?|\\x{1F3FC}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:[\\x{1F468}\\x{1F469}]|\\x{1F48B}\\x{200D}[\\x{1F468}\\x{1F469}])[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}[\\x{1F468}\\x{1F469}][\\x{1F3FB}\\x{1F3FD}-\\x{1F3FF}]))?|\\x{1F3FD}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:[\\x{1F468}\\x{1F469}]|\\x{1F48B}\\x{200D}[\\x{1F468}\\x{1F469}])[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}[\\x{1F468}\\x{1F469}][\\x{1F3FB}\\x{1F3FC}\\x{1F3FE}\\x{1F3FF}]))?|\\x{1F3FE}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:[\\x{1F468}\\x{1F469}]|\\x{1F48B}\\x{200D}[\\x{1F468}\\x{1F469}])[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}[\\x{1F468}\\x{1F469}][\\x{1F3FB}-\\x{1F3FD}\\x{1F3FF}]))?|\\x{1F3FF}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:[\\x{1F468}\\x{1F469}]|\\x{1F48B}\\x{200D}[\\x{1F468}\\x{1F469}])[\\x{1F3FB}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}[\\x{1F468}\\x{1F469}][\\x{1F3FB}-\\x{1F3FE}]))?)?|\\x{1F62E}(?:\\x{200D}\\x{1F4A8})?|\\x{1F635}(?:\\x{200D}\\x{1F4AB})?|\\x{1F636}(?:\\x{200D}\\x{1F32B}\\x{FE0F}?)?|\\x{1F642}(?:\\x{200D}[\\x{2194}\\x{2195}]\\x{FE0F}?)?|\\x{1F93C}(?:[\\x{1F3FB}-\\x{1F3FF}]|\\x{200D}[\\x{2640}\\x{2642}]\\x{FE0F}?)?|\\x{1F9D1}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{1F91D}\\x{200D}\\x{1F9D1}|\\x{1F9D1}\\x{200D}\\x{1F9D2}(?:\\x{200D}\\x{1F9D2})?|\\x{1F9D2}(?:\\x{200D}\\x{1F9D2})?)|\\x{1F3FB}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F9D1}[\\x{1F3FC}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FF}]))?|\\x{1F3FC}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F9D1}[\\x{1F3FB}\\x{1F3FD}-\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FF}]))?|\\x{1F3FD}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F9D1}[\\x{1F3FB}\\x{1F3FC}\\x{1F3FE}\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FF}]))?|\\x{1F3FE}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FD}\\x{1F3FF}]|\\x{1F91D}\\x{200D}\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FF}]))?|\\x{1F3FF}(?:\\x{200D}(?:[\\x{2695}\\x{2696}\\x{2708}]\\x{FE0F}?|[\\x{1F9AF}\\x{1F9BC}\\x{1F9BD}](?:\\x{200D}\\x{27A1}\\x{FE0F}?)?|[\\x{1F33E}\\x{1F373}\\x{1F37C}\\x{1F384}\\x{1F393}\\x{1F3A4}\\x{1F3A8}\\x{1F3EB}\\x{1F3ED}\\x{1F4BB}\\x{1F4BC}\\x{1F527}\\x{1F52C}\\x{1F680}\\x{1F692}\\x{1F9B0}-\\x{1F9B3}]|\\x{2764}\\x{FE0F}?\\x{200D}(?:\\x{1F48B}\\x{200D})?\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FE}]|\\x{1F91D}\\x{200D}\\x{1F9D1}[\\x{1F3FB}-\\x{1F3FF}]))?)?|\\x{1FAF1}(?:\\x{1F3FB}(?:\\x{200D}\\x{1FAF2}[\\x{1F3FC}-\\x{1F3FF}])?|\\x{1F3FC}(?:\\x{200D}\\x{1FAF2}[\\x{1F3FB}\\x{1F3FD}-\\x{1F3FF}])?|\\x{1F3FD}(?:\\x{200D}\\x{1FAF2}[\\x{1F3FB}\\x{1F3FC}\\x{1F3FE}\\x{1F3FF}])?|\\x{1F3FE}(?:\\x{200D}\\x{1FAF2}[\\x{1F3FB}-\\x{1F3FD}\\x{1F3FF}])?|\\x{1F3FF}(?:\\x{200D}\\x{1FAF2}[\\x{1F3FB}-\\x{1F3FE}])?)?"); public CustomEmojiPopupKeyboard(Activity activity, List emojis, String domain){ + this(activity, emojis, domain, false); + } + + public CustomEmojiPopupKeyboard(Activity activity, List emojis, String domain, boolean forReaction){ super(activity); this.emojis=emojis; this.domain=domain; + this.forReaction=forReaction; } @Override @@ -105,7 +121,15 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{ list.setClipToPadding(false); new StickyHeadersOverlay(activity, 0).install(list); - LinearLayout ll=new LinearLayout(activity); + LinearLayout ll=new LinearLayout(activity) { + @Override + public boolean onInterceptTouchEvent(MotionEvent e){ + if (e.getAction() == MotionEvent.ACTION_MOVE) { + getParent().requestDisallowInterceptTouchEvent(true); + } + return false; + } + }; ll.setOrientation(LinearLayout.VERTICAL); ll.setElevation(V.dp(3)); ll.setBackgroundResource(R.drawable.bg_m3_surface1); @@ -117,19 +141,52 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{ bottomPanel.setBackgroundResource(R.drawable.bg_m3_surface2); ll.addView(bottomPanel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - ImageButton hideKeyboard=new ImageButton(activity); - hideKeyboard.setImageResource(R.drawable.ic_fluent_keyboard_dock_24_regular); - hideKeyboard.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant))); - hideKeyboard.setBackgroundResource(R.drawable.bg_round_ripple); - hideKeyboard.setOnClickListener(v->hide()); - bottomPanel.addView(hideKeyboard, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.LEFT)); + if(forReaction){ + InputMethodManager imm=(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - ImageButton backspace=new ImageButton(activity); - backspace.setImageResource(R.drawable.ic_fluent_backspace_24_regular); - backspace.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant))); - backspace.setBackgroundResource(R.drawable.bg_round_ripple); - backspace.setOnClickListener(v->listener.onBackspace()); - bottomPanel.addView(backspace, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.RIGHT)); + // TODO: support filtering custom emoji + EditText input=new EditText(activity); + input.setHint(R.string.sk_enter_emoji_hint); + input.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (!s.toString().isEmpty()) { + if (emojiRegex.matcher(s.toString()).find()) { + imm.hideSoftInputFromWindow(input.getWindowToken(), 0); + listener.onEmojiSelected(s.toString().substring(before)); + input.getText().clear(); + } else { + Toast.makeText(activity, R.string.sk_enter_emoji_toast, Toast.LENGTH_SHORT).show(); + input.getText().clear(); + } + } + } + + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void afterTextChanged(Editable s) {} + }); + + FrameLayout.LayoutParams params=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START); + int pad=forReaction ? 0 : V.dp(36 + 16); + params.setMargins(pad, V.dp(8), pad, V.dp(8)); + bottomPanel.addView(input, params); + } + + if(!forReaction){ + ImageButton hideKeyboard=new ImageButton(activity); + hideKeyboard.setImageResource(R.drawable.ic_fluent_keyboard_dock_24_regular); + hideKeyboard.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant))); + hideKeyboard.setBackground(UiUtils.getThemeDrawable(activity, android.R.attr.actionBarItemBackground)); + hideKeyboard.setOnClickListener(v->hide()); + bottomPanel.addView(hideKeyboard, new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.START | Gravity.CENTER_VERTICAL)); + + ImageButton backspace=new ImageButton(activity); + backspace.setImageResource(R.drawable.ic_fluent_backspace_24_regular); + backspace.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant))); + backspace.setBackground(UiUtils.getThemeDrawable(activity, android.R.attr.actionBarItemBackground)); + backspace.setOnClickListener(v->listener.onBackspace()); + bottomPanel.addView(backspace, new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.END | Gravity.CENTER_VERTICAL)); + } return ll; } @@ -252,7 +309,8 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{ } public interface Listener{ - void onEmojiSelected(Emoji emoji); + void onEmojiSelected(Emoji customEmoji); + void onEmojiSelected(String emoji); void onBackspace(); } } 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 new file mode 100644 index 000000000..3690e3bb7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/EmojiReactionsStatusDisplayItem.java @@ -0,0 +1,267 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.app.Activity; +import android.graphics.Paint; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.joinmastodon.android.E; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.requests.statuses.AddStatusReaction; +import org.joinmastodon.android.api.requests.statuses.DeleteStatusReaction; +import org.joinmastodon.android.api.requests.statuses.PleromaAddStatusReaction; +import org.joinmastodon.android.api.requests.statuses.PleromaDeleteStatusReaction; +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.EmojiReaction; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.utils.TextDrawable; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.ListImageLoaderWrapper; +import me.grishka.appkit.imageloader.RecyclerViewDelegate; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { + public final Status status; + private final Drawable placeholder; + private List requests; + + public EmojiReactionsStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status) { + super(parentID, parentFragment); + this.status=status; + placeholder=parentFragment.getContext().getDrawable(R.drawable.image_placeholder).mutate(); + placeholder.setBounds(0, 0, V.sp(24), V.sp(24)); + } + + private void refresh(Holder holder) { + requests=status.reactions.stream() + .map(e->e.url!=null ? new UrlImageLoaderRequest(e.url, V.sp(24), V.sp(24)) : null) + .collect(Collectors.toList()); + holder.list.setPadding(holder.list.getPaddingLeft(), + status.reactions.isEmpty() ? 0 : V.dp(8), holder.list.getPaddingRight(), 0); + } + + @Override + public int getImageCount(){ + return (int) status.reactions.stream().filter(r->r.url != null).count(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + return requests.get(index); + } + + @Override + public Type getType(){ + return Type.EMOJI_REACTIONS; + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder { + private final UsableRecyclerView list; + + public Holder(Activity activity, ViewGroup parent) { + super(new UsableRecyclerView(activity) { + @Override + public boolean onTouchEvent(MotionEvent e){ + super.onTouchEvent(e); + // to pass through touch events (i.e. clicking the status) to the parent view + return false; + } + }); + list=(UsableRecyclerView) itemView; + list.setPadding(V.dp(12), 0, V.dp(12), 0); + list.setClipToPadding(false); + } + + @Override + public void onBind(EmojiReactionsStatusDisplayItem item) { + ListImageLoaderWrapper imgLoader=new ListImageLoaderWrapper(item.parentFragment.getContext(), list, new RecyclerViewDelegate(list), null); + list.setAdapter(new EmojiReactionsAdapter(this, imgLoader)); + list.setLayoutManager(new LinearLayoutManager(item.parentFragment.getContext(), LinearLayoutManager.HORIZONTAL, false)); + item.refresh(this); + } + + @Override + public void setImage(int index, Drawable image){ + View child=list.getChildAt(index); + if(child==null) return; + ((EmojiReactionViewHolder) list.getChildViewHolder(child)).setImage(index, image); + } + + @Override + public void clearImage(int index){ + setImage(index, item.placeholder); + } + + private class EmojiReactionsAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ + RecyclerView list; + ListImageLoaderWrapper imgLoader; + Holder parentHolder; + + public EmojiReactionsAdapter(Holder parentHolder, ListImageLoaderWrapper imgLoader){ + super(imgLoader); + this.parentHolder=parentHolder; + this.imgLoader=imgLoader; + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView list){ + super.onAttachedToRecyclerView(list); + this.list=list; + } + + @NonNull + @Override + public EmojiReactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + Button btn=new Button(parent.getContext(), null, 0, R.style.Widget_Mastodon_M3_Button_Outlined_Icon); + ViewGroup.MarginLayoutParams params=new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMarginEnd(V.dp(8)); + btn.setLayoutParams(params); + btn.setCompoundDrawableTintList(null); + btn.setBackgroundResource(R.drawable.bg_button_m3_tonal); + btn.setCompoundDrawables(item.placeholder, null, null, null); + return new EmojiReactionViewHolder(btn, item); + } + + @Override + public void onBindViewHolder(EmojiReactionViewHolder holder, int position){ + holder.bind(item.status.reactions.get(position)); + super.onBindViewHolder(holder, position); + } + + @Override + public int getItemCount(){ + return item.status.reactions.size(); + } + + @Override + public int getImageCountForItem(int position){ + return item.status.reactions.get(position).url == null ? 0 : 1; + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + return item.requests.get(position); + } + } + + private static class EmojiReactionViewHolder extends BindableViewHolder implements ImageLoaderViewHolder{ + private final Button btn; + private final EmojiReactionsStatusDisplayItem parent; + + public EmojiReactionViewHolder(@NonNull View itemView, EmojiReactionsStatusDisplayItem parent){ + super(itemView); + btn=(Button) itemView; + this.parent=parent; + } + + @Override + public void setImage(int index, Drawable drawable){ + drawable.setBounds(0, 0, V.sp(24), V.sp(24)); + btn.setCompoundDrawablesRelative(drawable, null, null, null); + if(drawable instanceof Animatable) ((Animatable) drawable).start(); + } + + @Override + public void clearImage(int index){ + setImage(index, parent.placeholder); + } + + @Override + public void onBind(EmojiReaction item){ + btn.setText(UiUtils.abbreviateNumber(item.count)); + btn.setContentDescription(item.name); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)btn.setTooltipText(item.name); + if(item.url==null){ + Paint p=new Paint(); + p.setTextSize(V.sp(18)); + TextDrawable drawable=new TextDrawable(p, item.name); + btn.setCompoundDrawablesRelative(drawable, null, null, null); + }else{ + btn.setCompoundDrawablesRelative(parent.placeholder, null, null, null); + } + btn.setSelected(item.me); + btn.setOnClickListener(e -> { + boolean deleting=item.me; + boolean ak=parent.parentFragment.isInstanceAkkoma(); + MastodonAPIRequest req = deleting + ? (ak ? new PleromaDeleteStatusReaction(parent.status.id, item.name) : new DeleteStatusReaction(parent.status.id, item.name)) + : (ak ? new PleromaAddStatusReaction(parent.status.id, item.name) : new AddStatusReaction(parent.status.id, item.name)); + req.setCallback(new Callback<>() { + @Override + public void onSuccess(Status result) { + List oldList=new ArrayList<>(parent.status.reactions); + parent.status.reactions.clear(); + parent.status.reactions.addAll(result.reactions); + EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter(); + + // this handles addition/removal of new reactions + UiUtils.updateList(oldList, result.reactions, adapter.list, adapter, + (e1, e2) -> e1.name.equals(e2.name)); + + // update the existing reactions' counts + for(int i=0; ir.name.equals(newReaction.name)).findAny().ifPresent(r->{ + if(newReaction.count!=r.count) adapter.notifyItemChanged(index); + }); + } + parent.refresh(adapter.parentHolder); + adapter.imgLoader.updateImages(); + E.post(new StatusCountersUpdatedEvent(result, adapter.parentHolder)); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(itemView.getContext()); + } + }) + .exec(parent.parentFragment.getAccountID()); + }); + + if (parent.parentFragment.isInstanceAkkoma()) { + // glitch-soc doesn't have this, afaik + btn.setOnLongClickListener(e->{ + EmojiReaction emojiReaction=parent.status.reactions.stream().filter(r->r.name.equals(item.name)).findAny().orElseThrow(); + Bundle args=new Bundle(); + args.putString("account", parent.parentFragment.getAccountID()); + args.putString("statusID", parent.status.id); + int atSymbolIndex = emojiReaction.name.indexOf("@"); + args.putString("emoji", atSymbolIndex != -1 ? emojiReaction.name.substring(0, atSymbolIndex) : emojiReaction.name); + args.putString("url", emojiReaction.url); + args.putInt("count", emojiReaction.count); + Nav.go(parent.parentFragment.getActivity(), StatusEmojiReactionsListFragment.class, args); + return true; + }); + } + } + } + } +} 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 f5bf05aa0..b1861ac70 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 @@ -6,6 +6,9 @@ import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -14,26 +17,38 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; +import android.widget.EditText; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.requests.statuses.AddStatusReaction; +import org.joinmastodon.android.api.requests.statuses.PleromaAddStatusReaction; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.ComposeFragment; +import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.StatusPrivacy; +import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; import java.util.function.Consumer; +import java.util.regex.Pattern; import me.grishka.appkit.Nav; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; @@ -54,8 +69,14 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends StatusDisplayItem.Holder{ + private final FrameLayout reactLayout; private final TextView replies, boosts, favorites; - private final View reply, boost, favorite, share, bookmark; + private final View reply, boost, favorite, share, bookmark, react; + private final InputMethodManager imm; + private CustomEmojiPopupKeyboard emojiKeyboard; + private LinearLayout emojiKeyboardContainer; + private boolean reactKeyboardVisible; + private final Activity activity; private static final Animation opacityOut, opacityIn; private View touchingView = null; @@ -77,18 +98,25 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ } }; + private static final float ALPHA_PRESSED=0.55f; + static { - opacityOut = new AlphaAnimation(1, 0.55f); + opacityOut = new AlphaAnimation(1, ALPHA_PRESSED); opacityOut.setDuration(300); opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT); opacityOut.setFillAfter(true); - opacityIn = new AlphaAnimation(0.55f, 1); + opacityIn = new AlphaAnimation(ALPHA_PRESSED, 1); opacityIn.setDuration(400); opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT); } public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_footer, parent); + this.activity = activity; + + reactLayout=findViewById(R.id.react_layout); + emojiKeyboardContainer=findViewById(R.id.footer_emoji_keyboard_container); + replies=findViewById(R.id.reply); boosts=findViewById(R.id.boost); favorites=findViewById(R.id.favorite); @@ -98,6 +126,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ favorite=findViewById(R.id.favorite_btn); share=findViewById(R.id.share_btn); bookmark=findViewById(R.id.bookmark_btn); + react=findViewById(R.id.react_btn); reply.setOnTouchListener(this::onButtonTouch); reply.setOnClickListener(this::onReplyClick); @@ -111,6 +140,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ favorite.setOnClickListener(this::onFavoriteClick); favorite.setOnLongClickListener(this::onFavoriteLongClick); favorite.setAccessibilityDelegate(buttonAccessibilityDelegate); + react.setOnTouchListener(this::onButtonTouch); + react.setOnClickListener(this::onReactClick); + react.setAccessibilityDelegate(buttonAccessibilityDelegate); bookmark.setOnTouchListener(this::onButtonTouch); bookmark.setOnClickListener(this::onBookmarkClick); bookmark.setOnLongClickListener(this::onBookmarkLongClick); @@ -119,6 +151,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ share.setOnClickListener(this::onShareClick); share.setOnLongClickListener(this::onShareLongClick); share.setAccessibilityDelegate(buttonAccessibilityDelegate); + + imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); } @Override @@ -135,6 +169,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ bookmark.setSelected(item.status.bookmarked); boost.setEnabled(item.status.isReblogPermitted(item.accountID)); + AccountSession accountSession=AccountSessionManager.get(item.accountID); + reactLayout.setVisibility(accountSession.getLocalPreferences().emojiReactionsEnabled + ? View.VISIBLE + : View.GONE); + int nextPos = getAbsoluteAdapterPosition() + 1; boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos && item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem; @@ -146,6 +185,28 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ condenseBottom ? V.dp(-5) : 0); itemView.requestLayout(); + + reactKeyboardVisible=false; + emojiKeyboard=new CustomEmojiPopupKeyboard(activity, AccountSessionManager.getInstance().getCustomEmojis(accountSession.domain), accountSession.domain, true); + emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){ + @Override + public void onEmojiSelected(Emoji emoji) { + addEmojiReaction(emoji.shortcode); + emojiKeyboard.toggleKeyboardPopup(null); + } + + @Override + public void onEmojiSelected(String emoji){ + addEmojiReaction(emoji); + emojiKeyboard.toggleKeyboardPopup(null); + } + + @Override + public void onBackspace() {} + }); + + emojiKeyboardContainer.removeAllViews(); + emojiKeyboardContainer.addView(emojiKeyboard.getView()); } private void bindText(TextView btn, long count){ @@ -324,6 +385,29 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ return true; } + private boolean resetReact(View v){ + if(!reactKeyboardVisible) return false; + if(emojiKeyboard.isVisible()) emojiKeyboard.toggleKeyboardPopup(null); + reactKeyboardVisible=false; + v.setAlpha(1); + v.startAnimation(opacityIn); + return true; + } + + private void onReactClick(View v){ + if (resetReact(v)) return; + reactKeyboardVisible=true; + emojiKeyboard.toggleKeyboardPopup(null); + DisplayMetrics displayMetrics = new DisplayMetrics(); + int[] locationOnScreen = new int[2]; + activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + v.getLocationOnScreen(locationOnScreen); + double fromScreenTop = (double) locationOnScreen[1] / displayMetrics.heightPixels; + if (fromScreenTop > 0.75) { + item.parentFragment.scrollBy(0, (int) (displayMetrics.heightPixels * 0.3)); + } + } + private void onBookmarkClick(View v){ bookmark.setSelected(!item.status.bookmarked); AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{ @@ -369,7 +453,29 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ return R.string.add_bookmark; if(id==R.id.share_btn) return R.string.button_share; + if(id==R.id.react_btn) + return R.string.sk_button_react; return 0; } + + private void addEmojiReaction(String emoji) { + MastodonAPIRequest req = item.parentFragment.isInstanceAkkoma() + ? new PleromaAddStatusReaction(item.status.id, emoji) + : new AddStatusReaction(item.status.id, emoji); + req.setCallback(new Callback<>() { + @Override + public void onSuccess(Status result) { + item.parentFragment.updateEmojiReactions(result, getItemID()); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(item.parentFragment.getContext()); + } + }) + .exec(item.accountID); + reactKeyboardVisible=false; + react.startAnimation(opacityIn); + } } } 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 cf5f6ba5c..5a905f7f9 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 @@ -64,6 +64,7 @@ public abstract class StatusDisplayItem{ public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3; public static final int FLAG_NO_HEADER=1 << 4; public static final int FLAG_NO_TRANSLATE=1 << 5; + public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6; public void setAncestryInfo( boolean hasDescendantNeighbor, @@ -102,6 +103,7 @@ public abstract class StatusDisplayItem{ case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); + case EMOJI_REACTIONS -> new EmojiReactionsStatusDisplayItem.Holder(activity, parent); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent); case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null)); @@ -118,7 +120,7 @@ public abstract class StatusDisplayItem{ }; } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, boolean disableTranslate, FilterContext filterContext) { + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean showReactions, boolean addFooter, boolean disableTranslate, FilterContext filterContext) { int flags=0; if(inset) flags|=FLAG_INSET; @@ -126,6 +128,8 @@ public abstract class StatusDisplayItem{ flags|=FLAG_NO_FOOTER; if (disableTranslate) flags|=FLAG_NO_TRANSLATE; + if (!showReactions) + flags|=FLAG_NO_EMOJI_REACTIONS; return buildItems(fragment, status, accountID, parentObject, knownAccounts, filterContext, flags); } @@ -204,7 +208,7 @@ public abstract class StatusDisplayItem{ items.add(replyLine); } } - + if((flags & FLAG_CHECKABLE)!=0) items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); else @@ -286,6 +290,9 @@ public abstract class StatusDisplayItem{ if(contentItems!=items && statusForContent.spoilerRevealed){ items.addAll(contentItems); } + if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && AccountSessionManager.get(accountID).getLocalPreferences().emojiReactionsEnabled){ + items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent)); + } if((flags & FLAG_NO_FOOTER)==0){ FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID); footer.hideCounts=hideCounts; @@ -340,6 +347,7 @@ public abstract class StatusDisplayItem{ POLL_OPTION, POLL_FOOTER, CARD, + EMOJI_REACTIONS, FOOTER, ACCOUNT_CARD, ACCOUNT, diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index ce47fe63f..9703bf623 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -195,11 +195,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ // remove additional padding when (transparently padded) translate button is visible int nextPos = getAbsoluteAdapterPosition() + 1; - boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos && - item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem; - int bottomPadding = (translateVisible && nextIsFooter) ? 0 - : nextIsFooter ? V.dp(6) - : V.dp(12); + int bottomPadding=V.dp(12); + if(item.parentFragment.getDisplayItems().size() > nextPos){ + if(item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem) bottomPadding=V.dp(6); + if(item.parentFragment.getDisplayItems().get(nextPos) instanceof EmojiReactionsStatusDisplayItem){ + bottomPadding=item.status.reactions.isEmpty() ? V.dp(6) : 0; + } + } itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding); if (!GlobalUserPreferences.collapseLongPosts) { diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/TextDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/TextDrawable.java new file mode 100644 index 000000000..c280cc938 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/TextDrawable.java @@ -0,0 +1,242 @@ +package org.joinmastodon.android.ui.utils; + +/* + * Copyright 2016 Ali Muzaffar + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +public class TextDrawable extends Drawable implements TextWatcher { + private WeakReference ref; + private String mText; + private Paint mPaint; + private Rect mHeightBounds; + private boolean mBindToViewPaint = false; + private float mPrevTextSize = 0; + private boolean mInitFitText = false; + private boolean mFitTextEnabled = false; + + /** + * Create a TextDrawable using the given paint object and string + * + * @param paint + * @param s + */ + public TextDrawable(Paint paint, String s) { + mText = s; + mPaint = new Paint(paint); + mHeightBounds = new Rect(); + init(); + } + + /** + * Create a TextDrawable. This uses the given TextView to initialize paint and has initial text + * that will be drawn. Initial text can also be useful for reserving space that may otherwise + * not be available when setting compound drawables. + * + * @param tv The TextView / EditText using to initialize this drawable + * @param initialText Optional initial text to display + * @param bindToViewsText Should this drawable mirror the text in the TextView + * @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc. + * Note, this will override any changes made using setColorFilter or setAlpha. + */ + public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) { + this(tv.getPaint(), initialText); + ref = new WeakReference<>(tv); + if (bindToViewsText || bindToViewsPaint) { + if (bindToViewsText) { + tv.addTextChangedListener(this); + } + mBindToViewPaint = bindToViewsPaint; + } + } + + /** + * Create a TextDrawable. This uses the given TextView to initialize paint and the text that + * will be drawn. + * + * @param tv The TextView / EditText using to initialize this drawable + * @param bindToViewsText Should this drawable mirror the text in the TextView + * @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc. + * Note, this will override any changes made using setColorFilter or setAlpha. + */ + public TextDrawable(TextView tv, boolean bindToViewsText, boolean bindToViewsPaint) { + this(tv, tv.getText().toString(), bindToViewsText, bindToViewsPaint); + } + + /** + * Use the provided TextView/EditText to initialize the drawable. + * The Drawable will copy the Text and the Paint properties, however it will from that + * point on be independant of the TextView. + * + * @param tv a TextView or EditText or any of their children. + */ + public TextDrawable(TextView tv) { + this(tv, false, false); + } + + /** + * Use the provided TextView/EditText to initialize the drawable. + * The Drawable will copy the Paint properties, and use the provided text to initialise itself. + * + * @param tv a TextView or EditText or any of their children. + * @param s The String to draw + */ + public TextDrawable(TextView tv, String s) { + this(tv, s, false, false); + } + + @Override + public void draw(Canvas canvas) { + if (mBindToViewPaint && ref.get() != null) { + Paint p = ref.get().getPaint(); + canvas.drawText(mText, 0, getBounds().height(), p); + } else { + if (mInitFitText) { + fitTextAndInit(); + } + canvas.drawText(mText, 0, getBounds().height(), mPaint); + } + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mPaint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + int alpha = mPaint.getAlpha(); + if (alpha == 0) { + return PixelFormat.TRANSPARENT; + } else if (alpha == 255) { + return PixelFormat.OPAQUE; + } else { + return PixelFormat.TRANSLUCENT; + } + } + + private void init() { + Rect bounds = getBounds(); + //We want to use some character to determine the max height of the text. + //Otherwise if we draw something like "..." they will appear centered + //Here I'm just going to use the entire alphabet to determine max height. + mPaint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, mHeightBounds); + //This doesn't account for leading or training white spaces. + //mPaint.getTextBounds(mText, 0, mText.length(), bounds); + float width = mPaint.measureText(mText); + bounds.top = mHeightBounds.top; + bounds.bottom = mHeightBounds.bottom; + bounds.right = (int) width; + bounds.left = 0; + setBounds(bounds); + } + + public void setPaint(Paint paint) { + mPaint = new Paint(paint); + //Since this can change the font used, we need to recalculate bounds. + if (mFitTextEnabled && !mInitFitText) { + fitTextAndInit(); + } else { + init(); + } + invalidateSelf(); + } + + public Paint getPaint() { + return mPaint; + } + + public void setText(String text) { + mText = text; + //Since this can change the bounds of the text, we need to recalculate. + if (mFitTextEnabled && !mInitFitText) { + fitTextAndInit(); + } else { + init(); + } + invalidateSelf(); + } + + public String getText() { + return mText; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + setText(s.toString()); + } + + /** + * Make the TextDrawable match the width of the View it's associated with. + *

+ * Note: While this option will not work if bindToViewPaint is true. + * + * @param fitText + */ + public void setFillText(boolean fitText) { + mFitTextEnabled = fitText; + if (fitText) { + mPrevTextSize = mPaint.getTextSize(); + if (ref.get() != null) { + if (ref.get().getWidth() > 0) { + fitTextAndInit(); + } else { + mInitFitText = true; + } + } + } else { + if (mPrevTextSize > 0) { + mPaint.setTextSize(mPrevTextSize); + } + init(); + } + } + + private void fitTextAndInit() { + float fitWidth = ref.get().getWidth(); + float textWidth = mPaint.measureText(mText); + float multi = fitWidth / textWidth; + mPaint.setTextSize(mPaint.getTextSize() * multi); + mInitFitText = false; + init(); + } + +} diff --git a/mastodon/src/main/res/layout/display_item_footer.xml b/mastodon/src/main/res/layout/display_item_footer.xml index a7fffce8b..93007cce1 100644 --- a/mastodon/src/main/res/layout/display_item_footer.xml +++ b/mastodon/src/main/res/layout/display_item_footer.xml @@ -7,156 +7,205 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - + android:rowCount="2" + android:columnCount="1"> - - - - - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:src="@drawable/ic_fluent_share_24_regular" + android:tint="?android:textColorSecondary" + android:gravity="center_vertical"/> - - + + - - + android:layout_height="wrap_content" + android:id="@+id/footer_emoji_keyboard_container" + android:orientation="vertical"> + + - \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings_sk.xml b/mastodon/src/main/res/values/strings_sk.xml index dff93af8f..2df387b8f 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -350,6 +350,18 @@ Notifications Profile Show tab labels in the navigation bar + Enable emoji reactions + Displays emoji reactions to posts and lets you interact with them. Some modified versions of Mastodon support this, but Mastodon doesn\'t. + Show emoji reactions in timelines + Whether emoji reactions should be displayed on timelines. If this option is off, emoji reactions will only be displayed when viewing a thread. + + One user reacted with %2$s + %1$,d users reacted with %2$s + + React with emoji + Tap again for System Keyboard + You need to type an emoji + Type to react with an emoji Duration Indefinite 5 minutes