Merge pull request #516

Iceshrimp improvements
This commit is contained in:
LucasGGamerM 2025-03-19 10:58:36 -03:00 committed by GitHub
commit ab72435347
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 439 additions and 74 deletions

View file

@ -37,6 +37,8 @@ import okhttp3.Response;
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
private static final String TAG="MastodonAPIRequest";
private static MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
private String domain;
private AccountSession account;
private String path;
@ -95,14 +97,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
public MastodonAPIRequest<T> execNoAuth(String domain){
this.domain=domain;
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
unauthenticatedApiController.submitRequest(this);
return this;
}
public MastodonAPIRequest<T> exec(String domain, Token token){
this.domain=domain;
this.token=token;
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
unauthenticatedApiController.submitRequest(this);
return this;
}

View file

@ -8,14 +8,23 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusMuted;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.ReblogDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import me.grishka.appkit.api.Callback;
@ -42,6 +51,9 @@ public class StatusInteractionController{
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
AccountSession session=AccountSessionManager.get(accountID);
Instance instance=session.getInstance().get();
SetStatusFavorited current=runningFavoriteRequests.remove(status.id);
if(current!=null){
current.cancel();
@ -54,6 +66,7 @@ public class StatusInteractionController{
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
cb.accept(result);
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, result.reactions, false, null));
}
@Override
@ -63,12 +76,58 @@ public class StatusInteractionController{
status.favourited=!favorited;
cb.accept(status);
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, status.reactions, false, null));
}
})
.exec(accountID);
runningFavoriteRequests.put(status.id, req);
status.favourited=favorited;
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(instance.configuration==null || instance.configuration.reactions==null)
return;
String defaultReactionEmojiRaw=instance.configuration.reactions.defaultReaction;
if(!instance.isIceshrimpJs() || defaultReactionEmojiRaw==null)
return;
boolean reactionIsCustom=defaultReactionEmojiRaw.startsWith(":");
String defaultReactionEmoji=reactionIsCustom ? defaultReactionEmojiRaw.substring(1, defaultReactionEmojiRaw.length()-1) : defaultReactionEmojiRaw;
ArrayList<EmojiReaction> reactions=new ArrayList<>(status.reactions.size());
for(EmojiReaction reaction:status.reactions){
reactions.add(reaction.copy());
}
Optional<EmojiReaction> existingReaction=reactions.stream().filter(r->r.me).findFirst();
Optional<EmojiReaction> existingDefaultReaction=reactions.stream().filter(r->r.name.equals(defaultReactionEmoji)).findFirst();
if(existingReaction.isPresent() && !favorited){
existingReaction.get().me=false;
existingReaction.get().count--;
existingReaction.get().pendingChange=true;
}else if(existingDefaultReaction.isPresent() && favorited){
existingDefaultReaction.get().count++;
existingDefaultReaction.get().me=true;
existingDefaultReaction.get().pendingChange=true;
}else if(favorited){
EmojiReaction reaction=null;
if(reactionIsCustom){
List<EmojiCategory> customEmojis=AccountSessionManager.getInstance().getCustomEmojis(session.domain);
for(EmojiCategory category:customEmojis){
for(Emoji emoji:category.emojis){
if(emoji.shortcode.equals(defaultReactionEmoji)){
reaction=EmojiReaction.of(emoji, session.self);
break;
}
}
}
if(reaction==null)
reaction=EmojiReaction.of(defaultReactionEmoji, session.self);
}else{
reaction=EmojiReaction.of(defaultReactionEmoji, session.self);
}
reaction.pendingChange=true;
reactions.add(reaction);
}
E.post(new EmojiReactionsUpdatedEvent(status.id, reactions, false, null));
}
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){

View file

@ -16,6 +16,7 @@ import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
@ -23,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class AccountLocalPreferences{
private final SharedPreferences prefs;
@ -72,19 +74,20 @@ public class AccountLocalPreferences{
// preReplySheet=prefs.getBoolean("preReplySheet", false);
// MEGALODON
Optional<Instance> instance=session.getInstance();
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", instance.map(Instance::isIceshrimp).orElse(false) ? ContentType.MISSKEY_MARKDOWN.name() : ContentType.PLAIN.name()));
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", instance.map(i->!i.isIceshrimp()).orElse(false));
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
glitchInstance=prefs.getBoolean("glitchInstance", false);
publishButtonText=prefs.getString("publishButtonText", null);
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", instance.map(i->i.isAkkoma() || i.isIceshrimp()).orElse(false));
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
color=prefs.contains("color") ? ColorPreference.valueOf(prefs.getString("color", null)) : null;
recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>());

View file

@ -71,7 +71,6 @@ public class AccountSessionManager{
private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>();
private HashMap<String, Long> instancesLastUpdated=new HashMap<>();
private HashMap<String, Instance> instances=new HashMap<>();
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
private Instance authenticatingInstance;
private Application authenticatingApp;
private String lastActiveAccountID;
@ -110,7 +109,7 @@ public class AccountSessionManager{
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
readInstanceInfo(domains);
maybeUpdateShortcuts();
}
@ -248,11 +247,6 @@ public class AccountSessionManager{
maybeUpdateShortcuts();
}
@NonNull
public MastodonAPIController getUnauthenticatedApiController(){
return unauthenticatedApiController;
}
public void authenticate(Activity activity, Instance instance){
authenticatingInstance=instance;
new CreateOAuthApp()

View file

@ -68,14 +68,14 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
instanceUser.url = "https://"+session.domain+"/about";
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
instanceUser.emojis = List.of();
Status fakeStatus = a.toStatus();
Status fakeStatus = a.toStatus(isInstanceIceshrimp());
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true);
textItem.textSelectable = true;
List<StatusDisplayItem> items=new ArrayList<>();
items.add(HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead));
items.add(textItem);
if(!isInstanceAkkoma()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true));
if(!isInstanceAkkoma() && !isInstanceIceshrimp()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true));
return items;
}

View file

@ -838,6 +838,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.invalidateItemDecorations();
}
public void onFavoriteChanged(Status status, String itemID) {
FooterStatusDisplayItem.Holder footer=findHolderOfType(itemID, FooterStatusDisplayItem.Holder.class);
if(footer!=null){
footer.getItem().status=status;
footer.onFavoriteClick();
}
}
@Override
public String getAccountID(){
return accountID;

View file

@ -927,6 +927,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
return false;
});
if(instance.isIceshrimpJs())
languageButton.setVisibility(View.GONE); // hide language selector on Iceshrimp-JS because the feature is not supported
if (!GlobalUserPreferences.relocatePublishButton)
publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth()));

View file

@ -22,6 +22,14 @@ public interface HasAccountID {
return getInstance().map(Instance::isPixelfed).orElse(false);
}
default boolean isInstanceIceshrimp() {
return getInstance().map(Instance::isIceshrimp).orElse(false);
}
default boolean isInstanceIceshrimpJs() {
return getInstance().map(Instance::isIceshrimpJs).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}

View file

@ -24,6 +24,7 @@ import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
@ -122,7 +123,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
Account self=AccountSessionManager.get(accountID).self;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS
|| (n.type==Notification.Type.REBLOG && !n.status.account.id.equals(self.id))){ // Iceshrimp quote
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
@ -316,13 +319,16 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){
for(Notification n : data){
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0; i<list.getChildCount(); i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && reactions.getItem().status==n.status.getContentStatus() && ev.viewHolder!=holder){
reactions.rebind();
}else if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(n.getID())){
reactions.updateReactions(ev.reactions);
}
}
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(n.getID())){
text.rebind();
}
}

View file

@ -991,7 +991,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
else hidePrivateNote();
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
notifyButton.setVisibility(relationship.following && !isInstanceIceshrimpJs() ? View.VISIBLE : View.GONE); // always hide notify button on Iceshrimp-JS because it's unsupported on the server
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());

View file

@ -327,13 +327,16 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){
for(Status s:data){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && reactions.getItem().status==s.getContentStatus() && ev.viewHolder!=holder){
reactions.rebind();
}else if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(s.getID())){
reactions.updateReactions(ev.reactions);
}
}
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(s.getID())){
text.rebind();
}
}

View file

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
@ -31,6 +32,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import java.util.Optional;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
@ -60,6 +64,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private String currentQuery;
private boolean disableDiscover;
private boolean isIceshrimp;
@Override
public void onCreate(Bundle savedInstanceState){
@ -78,13 +83,17 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[4];
Optional<Instance> instance=AccountSessionManager.get(accountID).getInstance();
disableDiscover=instance.map(Instance::isAkkoma).orElse(false);
isIceshrimp=instance.map(Instance::isIceshrimp).orElse(false);
tabViews=new FrameLayout[isIceshrimp ? 3 : 4]; // reduce array size on Iceshrimp to hide news feed because it's unsupported and always returns an empty list
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.discover_posts;
case 1 -> R.id.discover_hashtags;
case 2 -> R.id.discover_news;
case 2 -> isIceshrimp ? R.id.discover_users : R.id.discover_news; // skip unsupported news discovery on Iceshrimp
case 3 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
@ -126,10 +135,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment)
.add(R.id.discover_hashtags, hashtagsFragment);
if(!isIceshrimp) // skip unsupported news discovery on Iceshrimp
transaction.add(R.id.discover_news, newsFragment);
transaction
.add(R.id.discover_users, accountsFragment)
.commit();
}
@ -140,7 +152,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.hashtags;
case 2 -> R.string.news;
case 2 -> isIceshrimp ? R.string.for_you : R.string.news; // skip unsupported news discovery on Iceshrimp
case 3 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
@ -160,7 +172,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
disableDiscover=AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){
searchFragment=new SearchFragment();
@ -262,7 +273,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return switch(page){
case 0 -> postsFragment;
case 1 -> hashtagsFragment;
case 2 -> newsFragment;
case 2 -> isIceshrimp ? accountsFragment : newsFragment; // skip unsupported news discovery on Iceshrimp
case 3 -> accountsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};

View file

@ -54,7 +54,6 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
languageResolver.from(s.preferences.postingDefaultLanguage).orElse(null);
List<ListItem<Void>> items = new ArrayList<>(List.of(
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick),
customTabsItem=new ListItem<>(getString(R.string.settings_custom_tabs), getString(GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser), R.drawable.ic_fluent_open_24_regular, this::onCustomTabsClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, i->toggleCheckableItem(altTextItem)),
showPostsWithoutAltItem=new CheckableListItem<>(R.string.mo_settings_show_posts_without_alt, R.string.mo_settings_show_posts_without_alt_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showPostsWithoutAlt, R.drawable.ic_fluent_eye_tracking_on_24_regular, i->toggleCheckableItem(showPostsWithoutAltItem)),
@ -73,6 +72,11 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, i->toggleCheckableItem(showRepliesItem))
));
if(!isInstanceIceshrimpJs()) items.add(
0,
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick)
);
if(isInstanceAkkoma()) items.add(
replyVisibilityItem=new ListItem<>(R.string.sk_settings_reply_visibility, getReplyVisibilityString(), R.drawable.ic_fluent_chat_24_regular, this::onReplyVisibilityClick)
);

View file

@ -17,6 +17,7 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -35,24 +36,27 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
setTitle(R.string.sk_settings_instance);
AccountSession s=AccountSessionManager.get(accountID);
lp=s.getLocalPreferences();
onDataLoaded(List.of(
ArrayList<ListItem<Void>> items=new ArrayList<>(List.of(
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_fluent_server_24_regular, this::onServerClick),
new ListItem<>(R.string.sk_settings_profile, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/profile")),
new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")),
new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true),
contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, i->onContentTypeClick()),
defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true),
emojiReactionsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions, R.string.sk_settings_emoji_reactions_explanation, CheckableListItem.Style.SWITCH, lp.emojiReactionsEnabled, R.drawable.ic_fluent_emoji_laugh_24_regular, i->onEmojiReactionsClick()),
showEmojiReactionsItem=new ListItem<>(R.string.sk_settings_show_emoji_reactions, getShowEmojiReactionsString(), R.drawable.ic_fluent_emoji_24_regular, this::onShowEmojiReactionsClick, 0, true),
localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, i->onLocalOnlyClick()),
glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, i->toggleCheckableItem(glitchModeItem))
));
if(!isInstanceIceshrimp()){
items.add(4, contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, i->onContentTypeClick()));
items.add(5, defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true));
contentTypesItem.checkedChangeListener=checked->onContentTypeClick();
defaultContentTypeItem.isEnabled=contentTypesItem.checked;
}
emojiReactionsItem.checkedChangeListener=checked->onEmojiReactionsClick();
showEmojiReactionsItem.isEnabled=emojiReactionsItem.checked;
localOnlyItem.checkedChangeListener=checked->onLocalOnlyClick();
glitchModeItem.isEnabled=localOnlyItem.checked;
onDataLoaded(items);
}
@Override
@ -61,6 +65,7 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
@Override
protected void onHidden(){
super.onHidden();
if(contentTypesItem!=null)
lp.contentTypesEnabled=contentTypesItem.checked;
lp.emojiReactionsEnabled=emojiReactionsItem.checked;
lp.localOnlySupported=localOnlyItem.checked;
@ -84,7 +89,8 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
private void resetDefaultContentType(){
lp.defaultContentType=defaultContentTypeItem.isEnabled
? ContentType.PLAIN : ContentType.UNSPECIFIED;
? isInstanceIceshrimp() ? ContentType.MISSKEY_MARKDOWN
: ContentType.PLAIN : ContentType.UNSPECIFIED;
defaultContentTypeItem.subtitleRes=lp.defaultContentType.getName();
}

View file

@ -64,7 +64,7 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
));
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(!instance.isAkkoma()){
if(!instance.isAkkoma() && !instance.isIceshrimpJs()){ // hide filter settings on Akkoma and Iceshrimp-JS because the servers don't support the feature
data.add(3, new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_fluent_filter_24_regular, this::onFiltersClick));
}

View file

@ -50,11 +50,11 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
if(reactions==null) reactions=new ArrayList<>();
}
public Status toStatus() {
public Status toStatus(boolean isIceshrimp) {
Status s=Status.ofFake(id, content, publishedAt);
s.createdAt=startsAt != null ? startsAt : publishedAt;
s.reactions=reactions;
if(updatedAt != null) s.editedAt=updatedAt;
if(updatedAt != null && (!isIceshrimp || !updatedAt.equals(publishedAt))) s.editedAt=updatedAt;
return s;
}

View file

@ -34,6 +34,6 @@ public enum ContentType {
}
public boolean supportedByInstance(Instance i) {
return i.isAkkoma() || (this!=BBCODE && this!=MISSKEY_MARKDOWN);
return i.isAkkoma() || i.isIceshrimp() || (this!=BBCODE && this!=MISSKEY_MARKDOWN);
}
}

View file

@ -22,6 +22,7 @@ public class EmojiReaction {
public String staticUrl;
public transient ImageLoaderRequest request;
public transient boolean pendingChange=false;
public String getUrl(boolean playGifs){
String idealUrl=playGifs ? url : staticUrl;
@ -60,4 +61,18 @@ public class EmojiReaction {
accounts.add(self);
accountIds.add(self.id);
}
public EmojiReaction copy() {
EmojiReaction r=new EmojiReaction();
r.accounts=accounts;
r.accountIds=accountIds;
r.count=count;
r.me=me;
r.name=name;
r.url=url;
r.staticUrl=staticUrl;
r.request=request;
r.pendingChange=pendingChange;
return r;
}
}

View file

@ -146,14 +146,28 @@ public class Instance extends BaseModel{
return ci;
}
// This method has almost exclusively been used to improve support for
// Akkoma with no regard for Pleroma, hence its name. However, it is
// more likely than not that most uses should also apply to Pleroma,
// so checking for that too probably causes more good than harm.
public boolean isAkkoma() {
return pleroma != null;
return version.contains("compatible; Akkoma") || version.contains("compatible; Pleroma");
}
public boolean isPixelfed() {
return version.contains("compatible; Pixelfed");
}
// For both Iceshrimp-JS and Iceshrimp.NET
public boolean isIceshrimp() {
return version.contains("compatible; Iceshrimp");
}
// Only for Iceshrimp-JS
public boolean isIceshrimpJs() {
return version.contains("compatible; Iceshrimp "); // Iceshrimp.NET will not have a space immediately after
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)
@ -219,6 +233,7 @@ public class Instance extends BaseModel{
public StatusesConfiguration statuses;
public MediaAttachmentsConfiguration mediaAttachments;
public PollsConfiguration polls;
public ReactionsConfiguration reactions;
}
@Parcel
@ -246,6 +261,12 @@ public class Instance extends BaseModel{
public long maxExpiration;
}
@Parcel
public static class ReactionsConfiguration {
public int maxReactions;
public String defaultReaction;
}
@Parcel
public static class V2 extends BaseModel {
public V2.Configuration configuration;

View file

@ -1,5 +1,6 @@
package org.joinmastodon.android.ui.displayitems;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Paint;
@ -33,16 +34,24 @@ import org.joinmastodon.android.api.requests.statuses.PleromaDeleteStatusReactio
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.account_list.StatusEmojiReactionsListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.utils.TextDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.EmojiReactionButton;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@ -62,6 +71,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
private final boolean hideEmpty, forAnnouncement, playGifs;
private final String accountID;
private static final float ALPHA_DISABLED=0.55f;
private boolean forceShow=false;
public EmojiReactionsStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, String accountID, boolean hideEmpty, boolean forAnnouncement) {
super(parentID, parentFragment);
@ -90,6 +100,10 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
}
public boolean isHidden(){
if(forceShow){
forceShow=false;
return false;
}
return status.reactions.isEmpty() && hideEmpty;
}
@ -101,7 +115,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
vh.btn.setAlpha(visible ? ALPHA_DISABLED : 1);
}
private MastodonAPIRequest<?> createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Runnable cb, Runnable err){
private MastodonAPIRequest<?> createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Consumer<Status> cb, Runnable err){
setActionProgressVisible(vh, true);
boolean ak=parentFragment.isInstanceAkkoma();
boolean keepSpinning=delete && count == 1;
@ -113,7 +127,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
@Override
public void onSuccess(Object result){
if(!keepSpinning) setActionProgressVisible(vh, false);
cb.run();
cb.accept(null);
}
@Override
public void onError(ErrorResponse error){
@ -130,7 +144,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
@Override
public void onSuccess(Status result){
if(!keepSpinning) setActionProgressVisible(vh, false);
cb.run();
cb.accept(result);
}
@Override
public void onError(ErrorResponse error){
@ -151,6 +165,8 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
private final ProgressBar progress;
private final EmojiReactionsAdapter adapter;
private final ListImageLoaderWrapper imgLoader;
private int meReactionCount=0;
private Instance instance;
public Holder(Activity activity, ViewGroup parent) {
super(activity, R.layout.display_item_emoji_reactions, parent);
@ -171,6 +187,13 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
if(emojiKeyboard != null) root.removeView(emojiKeyboard.getView());
addButton.setSelected(false);
AccountSession session=item.parentFragment.getSession();
instance=item.parentFragment.getInstance().get();
if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0){
meReactionCount=(int) item.status.reactions.stream().filter(r->r.me).count();
boolean canReact=meReactionCount<instance.configuration.reactions.maxReactions;
addButton.setClickable(canReact);
addButton.setAlpha(canReact ? 1 : ALPHA_DISABLED);
}
item.status.reactions.forEach(r->r.request=r.getUrl(item.playGifs)!=null
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), 0, V.sp(24))
: null);
@ -182,17 +205,33 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
emojiKeyboard.setListener(this);
space.setVisibility(View.GONE);
root.addView(emojiKeyboard.getView());
boolean hidden=item.isHidden();
root.setVisibility(hidden ? View.GONE : View.VISIBLE);
line.setVisibility(hidden ? View.GONE : View.VISIBLE);
updateVisibility(item.isHidden(), true);
imgLoader.updateImages();
adapter.notifyDataSetChanged();
if(!GlobalUserPreferences.showDividers || item.isHidden())
return;
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
if(next!=null && !next.parentID.equals(item.parentID)) next=null;
if(next instanceof ExtendedFooterStatusDisplayItem)
itemView.setPadding(0, 0, 0, V.dp(12));
else
itemView.setPadding(0, 0, 0, 0);
}
private void updateVisibility(boolean hidden, boolean force){
int visibility=hidden ? View.GONE : View.VISIBLE;
if(!force && visibility==root.getVisibility())
return;
root.setVisibility(visibility);
line.setVisibility(visibility);
line.setPadding(
list.getPaddingLeft(),
hidden ? 0 : V.dp(8),
list.getPaddingRight(),
item.forAnnouncement ? V.dp(8) : 0
);
imgLoader.updateImages();
adapter.notifyDataSetChanged();
}
private void hideEmojiKeyboard(){
@ -244,19 +283,32 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
}
}
EmojiReaction finalExisting=existing;
item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, ()->{
item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, (status)->{
resetBtn.run();
if(finalExisting==null){
int pos=item.status.reactions.size();
int pos=status.reactions.stream()
.filter(r->r.name.equals(info!=null ? info.shortcode : emoji))
.findFirst()
.map(r->status.reactions.indexOf(r))
.orElse(item.status.reactions.size());
boolean previouslyEmpty=item.status.reactions.isEmpty();
item.status.reactions.add(pos, info!=null ? EmojiReaction.of(info, me) : EmojiReaction.of(emoji, me));
adapter.notifyItemRangeInserted(pos, 1);
if(previouslyEmpty)
adapter.notifyItemChanged(pos);
else
adapter.notifyItemInserted(pos);
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
scroller.setTargetPosition(pos);
list.getLayoutManager().startSmoothScroll(scroller);
updateMeReactionCount(false);
}else{
finalExisting.add(me);
adapter.notifyItemChanged(item.status.reactions.indexOf(finalExisting));
}
if(instance.isIceshrimpJs() && status!=null){
item.parentFragment.onFavoriteChanged(status, getItemID());
E.post(new StatusCountersUpdatedEvent(status));
}
E.post(new EmojiReactionsUpdatedEvent(item.status.id, item.status.reactions, countBefore==0, adapter.parentHolder));
}, resetBtn).exec(item.accountID);
}
@ -278,6 +330,99 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
}
}
private void updateAddButtonClickable() {
if(instance==null || instance.configuration==null || instance.configuration.reactions==null || instance.configuration.reactions.maxReactions==0)
return;
boolean canReact=meReactionCount<instance.configuration.reactions.maxReactions;
addButton.setClickable(canReact);
ObjectAnimator anim=ObjectAnimator.ofFloat(
addButton, View.ALPHA,
canReact ? ALPHA_DISABLED : 1,
canReact ? 1 : ALPHA_DISABLED);
anim.setDuration(200);
anim.start();
}
private void updateMeReactionCount(boolean deleting) {
meReactionCount=Math.max(0, meReactionCount + (deleting ? -1 : 1));
updateAddButtonClickable();
}
public void updateReactions(List<EmojiReaction> reactions){
item.status.reactions=new ArrayList<>(item.status.reactions); // I don't know how, but this seemingly fixes a bug
List<EmojiReaction> toRemove=new ArrayList<>();
for(int i=0;i<item.status.reactions.size();i++){
EmojiReaction reaction=item.status.reactions.get(i);
Optional<EmojiReaction> newReactionOptional=reactions.stream().filter(r->r.name.equals(reaction.name)).findFirst();
if(newReactionOptional.isEmpty()){ // deleted reactions
toRemove.add(reaction);
continue;
}
// changed reactions
EmojiReaction newReaction=newReactionOptional.get();
if(reaction.count!=newReaction.count || reaction.me!=newReaction.me || reaction.pendingChange!=newReaction.pendingChange){
if(newReaction.pendingChange){
View holderView=list.getChildAt(i);
if(holderView!=null){
EmojiReactionViewHolder reactionHolder=(EmojiReactionViewHolder) list.getChildViewHolder(holderView);
item.setActionProgressVisible(reactionHolder, true);
}
}else{
item.status.reactions.set(i, newReaction);
adapter.notifyItemChanged(i);
}
}
}
Collections.reverse(toRemove);
for(EmojiReaction r:toRemove){
int index=item.status.reactions.indexOf(r);
item.status.reactions.remove(index);
adapter.notifyItemRemoved(index);
}
boolean pendingAddReaction=false;
for(int i=0;i<reactions.size();i++){
EmojiReaction reaction=reactions.get(i);
if(item.status.reactions.stream().anyMatch(r->r.name.equals(reaction.name)))
continue;
// new reactions
if(reaction.pendingChange){
pendingAddReaction=true;
item.forceShow=true;
continue;
}
boolean previouslyEmpty=item.status.reactions.isEmpty();
item.status.reactions.add(i, reaction);
if(previouslyEmpty)
adapter.notifyItemChanged(i);
else
adapter.notifyItemInserted(i);
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
scroller.setTargetPosition(i);
list.getLayoutManager().startSmoothScroll(scroller);
}
if(pendingAddReaction){
progress.setVisibility(View.VISIBLE);
addButton.setClickable(false);
addButton.setAlpha(ALPHA_DISABLED);
}else{
progress.setVisibility(View.GONE);
}
int newMeReactionCount=(int) reactions.stream().filter(r->r.me || r.pendingChange).count();
if (newMeReactionCount!=meReactionCount){
meReactionCount=newMeReactionCount;
updateAddButtonClickable();
}
updateVisibility(reactions.isEmpty() && item.hideEmpty, false);
}
@Override
public void setImage(int index, Drawable image){
View child=list.getChildAt(index);
@ -330,7 +475,7 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
}
private static class EmojiReactionViewHolder extends BindableViewHolder<Pair<EmojiReactionsStatusDisplayItem, EmojiReaction>> implements ImageLoaderViewHolder{
private final ProgressBarButton btn;
private final EmojiReactionButton btn;
private final ProgressBar progress;
public EmojiReactionViewHolder(Context context, RecyclerView list){
@ -356,6 +501,12 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
@Override
public void onBind(Pair<EmojiReactionsStatusDisplayItem, EmojiReaction> item){
if(item.second.pendingChange){
itemView.setVisibility(View.GONE);
return;
}else{
itemView.setVisibility(View.VISIBLE);
}
item.first.setActionProgressVisible(this, false);
EmojiReactionsStatusDisplayItem parent=item.first;
EmojiReaction reaction=item.second;
@ -371,10 +522,25 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
btn.setCompoundDrawablesRelative(item.first.placeholder, null, null, null);
}
btn.setSelected(reaction.me);
if(parent.parentFragment.isInstanceIceshrimpJs() && reaction.name.contains("@")){
btn.setEnabled(false);
btn.setClickable(false);
btn.setLongClickable(true);
}else{
btn.setEnabled(true);
btn.setClickable(true);
}
btn.setOnClickListener(e->{
boolean deleting=reaction.me;
parent.createRequest(reaction.name, reaction.count, deleting, this, ()->{
EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter();
Instance instance = adapter.parentHolder.instance;
if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0 &&
adapter.parentHolder.meReactionCount >= instance.configuration.reactions.maxReactions &&
!reaction.me){
return;
}
boolean deleting=reaction.me;
parent.createRequest(reaction.name, reaction.count, deleting, this, (status)->{
for(int i=0; i<parent.status.reactions.size(); i++){
EmojiReaction r=parent.status.reactions.get(i);
if(!r.name.equals(reaction.name)) continue;
@ -394,6 +560,14 @@ public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
adapter.parentHolder.root.setVisibility(View.GONE);
adapter.parentHolder.line.setVisibility(View.GONE);
}
if(instance.configuration!=null && instance.configuration.reactions!=null && instance.configuration.reactions.maxReactions!=0){
adapter.parentHolder.updateMeReactionCount(deleting);
}
if(instance.isIceshrimpJs() && status!=null){
parent.parentFragment.onFavoriteChanged(status, adapter.parentHolder.getItemID());
E.post(new StatusCountersUpdatedEvent(status));
}
E.post(new EmojiReactionsUpdatedEvent(parent.status.id, parent.status.reactions, parent.status.reactions.isEmpty(), adapter.parentHolder));
adapter.parentHolder.imgLoader.updateImages();
}, null).exec(parent.parentFragment.getAccountID());

View file

@ -46,7 +46,6 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class FooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private final String accountID;
public boolean hideCounts;
@ -316,17 +315,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
UiUtils.opacityIn(v);
Bundle args=new Bundle();
args.putString("account", item.accountID);
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(item.accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
if(instance.pleroma == null){
Instance instance=AccountSessionManager.get(item.accountID).getInstance().get();
if(instance.isAkkoma() || instance.isIceshrimp()){
args.putParcelable("quote", Parcels.wrap(item.status));
}else{
StringBuilder prefilledText = new StringBuilder().append("\n\n");
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
prefilledText.append(item.status.url);
args.putString("prefilledText", prefilledText.toString());
args.putInt("selectionStart", 0);
}else{
args.putParcelable("quote", Parcels.wrap(item.status));
}
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
@ -335,6 +333,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return true;
}
public void onFavoriteClick() {
favorite.setSelected(item.status.favourited);
favorite.animate().scaleX(0.95f).scaleY(0.95f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
UiUtils.opacityOut(favorite);
favorite.postDelayed(() -> {
favorite.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
UiUtils.opacityIn(favorite);
if(item.status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
favorite.startAnimation(spin);
}
}, 300);
bindText(favorites, item.status.favouritesCount);
}
private void onFavoriteClick(View v){
if(item.status.preview) return;
applyInteraction(v, status -> {

View file

@ -113,7 +113,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null);
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt!=null ? a.startsAt : fakeStatus.createdAt, parentFragment, accountID, fakeStatus, null, null, null);
item.announcement = a;
item.consumeReadAnnouncement = consumeReadID;
return item;

View file

@ -276,6 +276,8 @@ public abstract class StatusDisplayItem{
if(statusForContent.quote!=null) {
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
if(quoteInlineIndex==-1)
quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br><br>RE:");
if(quoteInlineIndex!=-1)
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
else {

View file

@ -0,0 +1,34 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
public class EmojiReactionButton extends ProgressBarButton {
private final Handler handler=new Handler();
public EmojiReactionButton(Context context){
super(context);
}
public EmojiReactionButton(Context context, AttributeSet attrs){
super(context, attrs);
}
public EmojiReactionButton(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// allow long click even if button is disabled
int action=event.getAction();
if(action==MotionEvent.ACTION_DOWN && !isEnabled())
handler.postDelayed(this::performLongClick, ViewConfiguration.getLongPressTimeout());
if(action==MotionEvent.ACTION_UP)
handler.removeCallbacksAndMessages(null);
return super.onTouchEvent(event);
}
}

View file

@ -15,7 +15,7 @@
android:indeterminate="true"
android:outlineProvider="none"
android:visibility="gone"/>
<org.joinmastodon.android.ui.views.ProgressBarButton
<org.joinmastodon.android.ui.views.EmojiReactionButton
android:id="@+id/btn"
style="@style/Widget.Mastodon.M3.Button.Outlined.Icon"
android:layout_width="wrap_content"