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:
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class AddStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public AddStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.POST, "/statuses/" + id + "/react/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class DeleteStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public DeleteStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.POST, "/statuses/" + id + "/unreact/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class PleromaAddStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public PleromaAddStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.PUT, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class PleromaDeleteStatusReaction extends MastodonAPIRequest<Status> {
|
||||
public PleromaDeleteStatusReaction(String id, String emoji) {
|
||||
super(HttpMethod.DELETE, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PleromaGetStatusReactions extends MastodonAPIRequest<List<EmojiReaction>> {
|
||||
public PleromaGetStatusReactions(String id, String emoji) {
|
||||
super(HttpMethod.GET, "/pleroma/statuses/" + id + "/reactions/" + (emoji != null ? emoji : ""), new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -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<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EmojiReaction> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Parcel
|
||||
public class EmojiReaction {
|
||||
public List<Account> accounts;
|
||||
public List<String> accountIds;
|
||||
public int count;
|
||||
public boolean me;
|
||||
public String name;
|
||||
public String url;
|
||||
public String staticUrl;
|
||||
}
|
||||
@@ -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<EmojiReaction> reactions;
|
||||
protected List<EmojiReaction> 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(){
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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<ImageLoaderRequest> 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<EmojiReactionsStatusDisplayItem> 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<EmojiReactionViewHolder> 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<EmojiReaction> 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<Status> 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<EmojiReaction> 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; i<result.reactions.size(); i++){
|
||||
int index=i;
|
||||
EmojiReaction newReaction=result.reactions.get(index);
|
||||
oldList.stream().filter(r->r.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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FooterStatusDisplayItem>{
|
||||
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<Status> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, boolean disableTranslate, FilterContext filterContext) {
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> 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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
/*
|
||||
* Copyright 2016 Ali Muzaffar
|
||||
* <p/>
|
||||
* 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
|
||||
* <p/>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p/>
|
||||
* 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<TextView> 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.
|
||||
* <p/>
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,156 +7,205 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
<GridLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="11sp">
|
||||
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
|
||||
android:rowCount="2"
|
||||
android:columnCount="1">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/reply_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/reply"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="11sp">
|
||||
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/boost_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:foregroundTint="@color/boost_icon"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_boost"
|
||||
android:tint="@color/boost_icon"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/boost"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textColor="@color/boost_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/favorite_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_fluent_star_24_selector"
|
||||
android:tint="@color/favorite_icon"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/favorite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:textColor="@color/favorite_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<FrameLayout
|
||||
android:id="@+id/bookmark_btn"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/reply_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/reply"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/boost_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:foregroundTint="@color/boost_icon"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_boost"
|
||||
android:tint="@color/boost_icon"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/boost"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textColor="@color/boost_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/favorite_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:src="@drawable/ic_fluent_star_24_selector"
|
||||
android:tint="@color/favorite_icon"
|
||||
android:gravity="center_vertical" />
|
||||
<TextView
|
||||
android:id="@+id/favorite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:minWidth="16dp"
|
||||
android:textColor="@color/favorite_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/react_layout"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<FrameLayout
|
||||
android:id="@+id/react_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/react"
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:src="@drawable/ic_fluent_emoji_laugh_24_regular"
|
||||
android:tint="@color/bookmark_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large" />
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
<EditText
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="20dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:id="@+id/react_input"/>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent">
|
||||
<FrameLayout
|
||||
android:id="@+id/bookmark_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/bookmark"
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:src="@drawable/ic_fluent_bookmark_24_selector"
|
||||
android:tint="@color/bookmark_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/share_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/bookmark"
|
||||
android:id="@+id/share"
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:src="@drawable/ic_fluent_bookmark_24_selector"
|
||||
android:tint="@color/bookmark_icon"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_label_large" />
|
||||
android:src="@drawable/ic_fluent_share_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:gravity="center_vertical"/>
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/share_btn"
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingVertical="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/share"
|
||||
android:layout_width="24sp"
|
||||
android:layout_height="24sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:src="@drawable/ic_fluent_share_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:gravity="center_vertical"/>
|
||||
</FrameLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/footer_emoji_keyboard_container"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
</GridLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
|
||||
@@ -350,6 +350,18 @@
|
||||
<string name="sk_tab_notifications">Notifications</string>
|
||||
<string name="sk_tab_profile">Profile</string>
|
||||
<string name="sk_settings_show_labels_in_navigation_bar">Show tab labels in the navigation bar</string>
|
||||
<string name="sk_settings_emoji_reactions">Enable emoji reactions</string>
|
||||
<string name="sk_settings_emoji_reactions_explanation">Displays emoji reactions to posts and lets you interact with them. Some modified versions of Mastodon support this, but Mastodon doesn\'t.</string>
|
||||
<string name="sk_settings_emoji_reactions_in_lists">Show emoji reactions in timelines</string>
|
||||
<string name="sk_settings_emoji_reactions_in_lists_explanation">Whether emoji reactions should be displayed on timelines. If this option is off, emoji reactions will only be displayed when viewing a thread.</string>
|
||||
<plurals name="sk_users_reacted_with">
|
||||
<item quantity="one">One user reacted with %2$s</item>
|
||||
<item quantity="other">%1$,d users reacted with %2$s</item>
|
||||
</plurals>
|
||||
<string name="sk_button_react">React with emoji</string>
|
||||
<string name="sk_again_for_system_keyboard">Tap again for System Keyboard</string>
|
||||
<string name="sk_enter_emoji_toast">You need to type an emoji</string>
|
||||
<string name="sk_enter_emoji_hint">Type to react with an emoji</string>
|
||||
<string name="sk_mute_label">Duration</string>
|
||||
<string name="sk_duration_indefinite">Indefinite</string>
|
||||
<string name="sk_duration_minutes_5">5 minutes</string>
|
||||
|
||||
Reference in New Issue
Block a user