Emoji Reactions Support (#645)
* Display Pleroma emoji reactions * Interact with existing Pleroma emoji reactions * Setting for emoji reaction support * Setting for displaying reactions in timelines * More horizontal padding on reactions display item * List accounts who reacted * Arbitrary emoji reaction from status footer * Hide custom emoji keyboard when emoji is selected * Clear preferences before applying All preferences get written anyways so nothing will be lost * Reset react visibility state on bind * Fix custom emoji turning black when reacting * Load reactions when a new one is added * Emoji reactions grid * Load custom emoji in reactions list fragment * New reaction toast messages and Unicode emoji regex * Make custom emoji picker for reactions scrollable * Scroll down to show custom emoji picker when reacting * Divider after reaction custom emoji picker * Animate react button opacity back in * fix plural strings * re-implement reactions using horizontal recycler view * update reactions with event * tweak emoji font size * tweak button styles (a tiny bit) * change footer react button behavior * fix emoji reaction status item padding * move emoji reactions below content items * add content description and tooltip * use custom emoji keyboard to enter unicode emoji * fix reactions clearing on status counter updates * fix space next to emoji reactions not clickable * make compatible with glitch-soc * Remove now unused EmojiReactionsView class * improve handling of reaction padding --------- Co-authored-by: sk <sk22@mailbox.org>
This commit is contained in:
@@ -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<T extends DisplayItemsParent> 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<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollBy(int x, int y) {
|
||||
list.scrollBy(x, y);
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<Notificati
|
||||
return items;
|
||||
}
|
||||
if(n.status!=null){
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); // | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); // | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
if (GlobalUserPreferences.spectatorMode)
|
||||
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
|
||||
if (!getLocalPrefs().showEmojiReactionsInLists)
|
||||
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
@@ -254,6 +259,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
footer.rebind();
|
||||
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||
footer.rebind();
|
||||
}else if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && ev.viewHolder!=holder){
|
||||
reactions.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> 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
|
||||
|
||||
@@ -57,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
|
||||
int idx=data.indexOf(s);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<Status>
|
||||
protected EventListener eventListener=new EventListener();
|
||||
|
||||
protected List<StatusDisplayItem> 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<Status>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.PleromaGetStatusReactions;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class StatusEmojiReactionsListFragment extends BaseAccountListFragment {
|
||||
private String id;
|
||||
private String emojiName;
|
||||
private String url;
|
||||
private int count;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
id = getArguments().getString("statusID");
|
||||
emojiName = getArguments().getString("emoji");
|
||||
url = getArguments().getString("url");
|
||||
count = getArguments().getInt("count");
|
||||
|
||||
SpannableStringBuilder title = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.sk_users_reacted_with, count,
|
||||
count, url == null ? emojiName : ":"+emojiName+":"));
|
||||
if (url != null) {
|
||||
Emoji emoji = new Emoji();
|
||||
emoji.shortcode = emojiName;
|
||||
emoji.url = url;
|
||||
HtmlParser.parseCustomEmoji(title, Collections.singletonList(emoji));
|
||||
}
|
||||
setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (url != null) {
|
||||
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dataLoaded() {
|
||||
super.dataLoaded();
|
||||
footerProgress.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest = new PleromaGetStatusReactions(id, emojiName)
|
||||
.setCallback(new SimpleCallback<>(StatusEmojiReactionsListFragment.this){
|
||||
@Override
|
||||
public void onSuccess(List<EmojiReaction> result) {
|
||||
if (getActivity() == null)
|
||||
return;
|
||||
|
||||
List<AccountViewModel> items = result.get(0).accounts.stream()
|
||||
.map(a -> new AccountViewModel(a, accountID))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
super.onError(error);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(){
|
||||
super.onResume();
|
||||
if(!loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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<SearchResult>{
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import java.util.List;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class SettingsInstanceFragment extends BaseSettingsFragment<Void> implements HasAccountID{
|
||||
private CheckableListItem<Void> contentTypesItem, localOnlyItem, glitchModeItem;
|
||||
private CheckableListItem<Void> contentTypesItem, emojiReactionsItem, emojiReactionsInListsItem, localOnlyItem, glitchModeItem;
|
||||
private ListItem<Void> defaultContentTypeItem;
|
||||
private AccountLocalPreferences lp;
|
||||
|
||||
@@ -36,11 +36,15 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> 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<Void> 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<Void> 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();
|
||||
|
||||
Reference in New Issue
Block a user