diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java index 424a284bd..6764259e4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java +++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java @@ -23,6 +23,7 @@ import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Mention; +import org.joinmastodon.android.model.NotificationType; import org.joinmastodon.android.model.PushNotification; import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.utils.UiUtils; @@ -183,7 +184,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{ builder.setSubText(accountName); } String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id); - if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){ + if(notification!=null && (notification.type==NotificationType.MENTION)){ ArrayList mentions=new ArrayList<>(); String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id; if(!notification.status.account.id.equals(ownID)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java index ed1e46cf2..7ce7fbd44 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -14,26 +14,35 @@ import com.google.gson.reflect.TypeToken; import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.api.requests.lists.GetLists; -import org.joinmastodon.android.api.requests.notifications.GetNotifications; +import org.joinmastodon.android.api.requests.notifications.GetNotificationsV1; +import org.joinmastodon.android.api.requests.notifications.GetNotificationsV2; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FollowList; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.NotificationGroup; +import org.joinmastodon.android.model.NotificationType; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.viewmodel.NotificationViewModel; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; @@ -41,7 +50,7 @@ import me.grishka.appkit.utils.WorkerThread; public class CacheController{ private static final String TAG="CacheController"; - private static final int DB_VERSION=4; + private static final int DB_VERSION=5; public static final WorkerThread databaseThread=new WorkerThread("databaseThread"); public static final Handler uiHandler=new Handler(Looper.getMainLooper()); @@ -49,7 +58,7 @@ public class CacheController{ private DatabaseHelper db; private final Runnable databaseCloseRunnable=this::closeDatabase; private boolean loadingNotifications; - private final ArrayList>>> pendingNotificationsCallbacks=new ArrayList<>(); + private final ArrayList>>> pendingNotificationsCallbacks=new ArrayList<>(); private List lists; private static final int POST_FLAG_GAP_AFTER=1; @@ -133,75 +142,176 @@ public class CacheController{ }); } - public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback>> callback){ + private List makeNotificationViewModels(List notifications, Map accounts, Map statuses){ + return notifications.stream() + .filter(ng->ng.type!=null) + .map(ng->{ + NotificationViewModel nvm=new NotificationViewModel(); + nvm.notification=ng; + nvm.accounts=ng.sampleAccountIds.stream().map(accounts::get).collect(Collectors.toList()); + if(nvm.accounts.size()!=ng.sampleAccountIds.size()) + return null; + if(ng.statusId!=null){ + nvm.status=statuses.get(ng.statusId); + if(nvm.status==null) + return null; + } + return nvm; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback>> callback){ cancelDelayedClose(); databaseThread.postRunnable(()->{ try{ - if(!onlyMentions && loadingNotifications){ - synchronized(pendingNotificationsCallbacks){ - pendingNotificationsCallbacks.add(callback); - } - return; - } if(!forceReload){ SQLiteDatabase db=getOrOpenDatabase(); - try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id` result=new ArrayList<>(); + ArrayList result=new ArrayList<>(); cursor.moveToFirst(); String newMaxID; + HashSet needAccounts=new HashSet<>(), needStatuses=new HashSet<>(); do{ - Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class); + NotificationGroup ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), NotificationGroup.class); ntf.postprocess(); - newMaxID=ntf.id; + newMaxID=ntf.pageMinId; + needAccounts.addAll(ntf.sampleAccountIds); + if(ntf.statusId!=null) + needStatuses.add(ntf.statusId); result.add(ntf); }while(cursor.moveToNext()); String _newMaxID=newMaxID; - AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS); - uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID))); + HashMap accounts=new HashMap<>(); + HashMap statuses=new HashMap<>(); + if(!needAccounts.isEmpty()){ + try(Cursor cursor2=db.query(accountsTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needAccounts.size(), "?"))+")", + needAccounts.toArray(new String[0]), null, null, null)){ + while(cursor2.moveToNext()){ + Account acc=MastodonAPIController.gson.fromJson(cursor2.getString(0), Account.class); + acc.postprocess(); + accounts.put(acc.id, acc); + } + } + } + if(!needStatuses.isEmpty()){ + try(Cursor cursor2=db.query(statusesTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needStatuses.size(), "?"))+")", + needStatuses.toArray(new String[0]), null, null, null)){ + while(cursor2.moveToNext()){ + Status s=MastodonAPIController.gson.fromJson(cursor2.getString(0), Status.class); + s.postprocess(); + statuses.put(s.id, s); + } + } + } + uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(makeNotificationViewModels(result, accounts, statuses), _newMaxID))); return; } }catch(IOException x){ Log.w(TAG, "getNotifications: corrupted notification object in database", x); } } + + if(!onlyMentions && loadingNotifications){ + synchronized(pendingNotificationsCallbacks){ + pendingNotificationsCallbacks.add(callback); + } + return; + } if(!onlyMentions) loadingNotifications=true; - new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class)) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List result){ - ArrayList filtered=new ArrayList<>(result); - AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS); - PaginatedResponse> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id); - callback.onSuccess(res); - putNotifications(result, onlyMentions, maxID==null); - if(!onlyMentions){ - loadingNotifications=false; - synchronized(pendingNotificationsCallbacks){ - for(Callback>> cb:pendingNotificationsCallbacks){ - cb.onSuccess(res); + if(AccountSessionManager.get(accountID).getInstanceInfo().getApiVersion()>=2){ + new GetNotificationsV2(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(GetNotificationsV2.GroupedNotificationsResults result){ + Map accounts=result.accounts.stream().collect(Collectors.toMap(a->a.id, Function.identity(), (a1, a2)->a2)); + Map statuses=result.statuses.stream().collect(Collectors.toMap(s->s.id, Function.identity(), (s1, s2)->s2)); + List notifications=makeNotificationViewModels(result.notificationGroups, accounts, statuses); + databaseThread.postRunnable(()->putNotifications(result.notificationGroups, result.accounts, result.statuses, onlyMentions, maxID==null), 0); + PaginatedResponse> res=new PaginatedResponse<>(notifications, + result.notificationGroups.isEmpty() ? null : result.notificationGroups.get(result.notificationGroups.size()-1).pageMinId); + callback.onSuccess(res); + if(!onlyMentions){ + loadingNotifications=false; + synchronized(pendingNotificationsCallbacks){ + for(Callback>> cb:pendingNotificationsCallbacks){ + cb.onSuccess(res); + } + pendingNotificationsCallbacks.clear(); } - pendingNotificationsCallbacks.clear(); } } - } - @Override - public void onError(ErrorResponse error){ - callback.onError(error); - if(!onlyMentions){ - loadingNotifications=false; - synchronized(pendingNotificationsCallbacks){ - for(Callback>> cb:pendingNotificationsCallbacks){ - cb.onError(error); + @Override + public void onError(ErrorResponse error){ + callback.onError(error); + if(!onlyMentions){ + loadingNotifications=false; + synchronized(pendingNotificationsCallbacks){ + for(Callback>> cb:pendingNotificationsCallbacks){ + cb.onError(error); + } + pendingNotificationsCallbacks.clear(); } - pendingNotificationsCallbacks.clear(); } } - } - }) - .exec(accountID); + }) + .exec(accountID); + }else{ + new GetNotificationsV1(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class)) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + ArrayList filtered=new ArrayList<>(result); + AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS); + List statuses=filtered.stream().map(n->n.status).filter(Objects::nonNull).collect(Collectors.toList()); + List accounts=filtered.stream().map(n->n.account).collect(Collectors.toList()); + List converted=filtered.stream() + .map(n->{ + NotificationGroup group=new NotificationGroup(); + group.groupKey="converted-"+n.id; + group.notificationsCount=1; + group.type=n.type; + group.mostRecentNotificationId=group.pageMaxId=group.pageMinId=n.id; + group.latestPageNotificationAt=n.createdAt; + group.sampleAccountIds=List.of(n.account.id); + if(n.status!=null) + group.statusId=n.status.id; + NotificationViewModel nvm=new NotificationViewModel(); + nvm.notification=group; + nvm.status=n.status; + nvm.accounts=List.of(n.account); + return nvm; + }) + .collect(Collectors.toList()); + PaginatedResponse> res=new PaginatedResponse<>(converted, result.isEmpty() ? null : result.get(result.size()-1).id); + callback.onSuccess(res); + databaseThread.postRunnable(()->putNotifications(converted.stream().map(nvm->nvm.notification).collect(Collectors.toList()), accounts, statuses, onlyMentions, maxID==null), 0); + } + + @Override + public void onError(ErrorResponse error){ + callback.onError(error); + if(!onlyMentions){ + loadingNotifications=false; + synchronized(pendingNotificationsCallbacks){ + for(Callback>> cb:pendingNotificationsCallbacks){ + cb.onError(error); + } + pendingNotificationsCallbacks.clear(); + } + } + } + }) + .exec(accountID); + } }catch(SQLiteException x){ Log.w(TAG, x); uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x))); @@ -211,22 +321,40 @@ public class CacheController{ }, 0); } - private void putNotifications(List notifications, boolean onlyMentions, boolean clear){ + private void putNotifications(List notifications, List accounts, List statuses, boolean onlyMentions, boolean clear){ runOnDbThread((db)->{ - String table=onlyMentions ? "notifications_mentions" : "notifications_all"; - if(clear) + String suffix=onlyMentions ? "mentions" : "all"; + String table="notifications_"+suffix; + String accountsTable="notifications_accounts_"+suffix; + String statusesTable="notifications_statuses_"+suffix; + if(clear){ db.delete(table, null, null); + db.delete(accountsTable, null, null); + db.delete(statusesTable, null, null); + } ContentValues values=new ContentValues(4); - for(Notification n:notifications){ + for(NotificationGroup n:notifications){ if(n.type==null){ continue; } - values.put("id", n.id); + values.put("id", n.groupKey); values.put("json", MastodonAPIController.gson.toJson(n)); values.put("type", n.type.ordinal()); - values.put("time", n.createdAt.getEpochSecond()); + values.put("time", n.latestPageNotificationAt.getEpochSecond()); + values.put("max_id", n.pageMaxId); db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); } + values.clear(); + for(Account acc:accounts){ + values.put("id", acc.id); + values.put("json", MastodonAPIController.gson.toJson(acc)); + db.insertWithOnConflict(accountsTable, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + for(Status s:statuses){ + values.put("id", s.id); + values.put("json", MastodonAPIController.gson.toJson(s)); + db.insertWithOnConflict(statusesTable, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } }); } @@ -409,22 +537,8 @@ public class CacheController{ `flags` INTEGER NOT NULL DEFAULT 0, `time` INTEGER NOT NULL )"""); - db.execSQL(""" - CREATE TABLE `notifications_all` ( - `id` VARCHAR(25) NOT NULL PRIMARY KEY, - `json` TEXT NOT NULL, - `flags` INTEGER NOT NULL DEFAULT 0, - `type` INTEGER NOT NULL, - `time` INTEGER NOT NULL - )"""); - db.execSQL(""" - CREATE TABLE `notifications_mentions` ( - `id` VARCHAR(25) NOT NULL PRIMARY KEY, - `json` TEXT NOT NULL, - `flags` INTEGER NOT NULL DEFAULT 0, - `type` INTEGER NOT NULL, - `time` INTEGER NOT NULL - )"""); + createNotificationsTables(db, "all"); + createNotificationsTables(db, "mentions"); createRecentSearchesTable(db); createMiscTable(db); } @@ -440,6 +554,12 @@ public class CacheController{ if(oldVersion<4){ createMiscTable(db); } + if(oldVersion<5){ + db.execSQL("DROP TABLE `notifications_all`"); + db.execSQL("DROP TABLE `notifications_mentions`"); + createNotificationsTables(db, "all"); + createNotificationsTables(db, "mentions"); + } } private void createRecentSearchesTable(SQLiteDatabase db){ @@ -467,5 +587,28 @@ public class CacheController{ `value` TEXT )"""); } + + private void createNotificationsTables(SQLiteDatabase db, String suffix){ + db.execSQL("CREATE TABLE `notifications_"+suffix+"` ("+ + """ + `id` VARCHAR(100) NOT NULL PRIMARY KEY, + `json` TEXT NOT NULL, + `flags` INTEGER NOT NULL DEFAULT 0, + `type` INTEGER NOT NULL, + `time` INTEGER NOT NULL, + `max_id` VARCHAR(25) NOT NULL + )"""); + db.execSQL("CREATE INDEX `notifications_"+suffix+"_max_id` ON `notifications_"+suffix+"`(`max_id`)"); + db.execSQL("CREATE TABLE `notifications_accounts_"+suffix+"` ("+ + """ + `id` VARCHAR(25) NOT NULL PRIMARY KEY, + `json` TEXT NOT NULL + )"""); + db.execSQL("CREATE TABLE `notifications_statuses_"+suffix+"` ("+ + """ + `id` VARCHAR(25) NOT NULL PRIMARY KEY, + `json` TEXT NOT NULL + )"""); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV1.java similarity index 69% rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV1.java index 19b4c8adc..9b07546ca 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV1.java @@ -7,26 +7,27 @@ import com.google.gson.reflect.TypeToken; import org.joinmastodon.android.api.ApiUtils; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.NotificationType; import java.util.EnumSet; import java.util.List; -public class GetNotifications extends MastodonAPIRequest>{ - public GetNotifications(String maxID, int limit, EnumSet includeTypes){ +public class GetNotificationsV1 extends MastodonAPIRequest>{ + public GetNotificationsV1(String maxID, int limit, EnumSet includeTypes){ this(maxID, limit, includeTypes, null); } - public GetNotifications(String maxID, int limit, EnumSet includeTypes, String onlyAccountID){ + public GetNotificationsV1(String maxID, int limit, EnumSet includeTypes, String onlyAccountID){ super(HttpMethod.GET, "/notifications", new TypeToken<>(){}); if(maxID!=null) addQueryParameter("max_id", maxID); if(limit>0) addQueryParameter("limit", ""+limit); if(includeTypes!=null){ - for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){ + for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){ addQueryParameter("types[]", type); } - for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), Notification.Type.class)){ + for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){ addQueryParameter("exclude_types[]", type); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV2.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV2.java new file mode 100644 index 000000000..3ad7d3b4f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotificationsV2.java @@ -0,0 +1,69 @@ +package org.joinmastodon.android.api.requests.notifications; + +import android.text.TextUtils; + +import org.joinmastodon.android.api.AllFieldsAreRequired; +import org.joinmastodon.android.api.ApiUtils; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.BaseModel; +import org.joinmastodon.android.model.NotificationGroup; +import org.joinmastodon.android.model.NotificationType; +import org.joinmastodon.android.model.Status; + +import java.util.EnumSet; +import java.util.List; + +public class GetNotificationsV2 extends MastodonAPIRequest{ + public GetNotificationsV2(String maxID, int limit, EnumSet includeTypes, EnumSet groupedTypes){ + this(maxID, limit, includeTypes, groupedTypes, null); + } + + public GetNotificationsV2(String maxID, int limit, EnumSet includeTypes, EnumSet groupedTypes, String onlyAccountID){ + super(HttpMethod.GET, "/notifications", GroupedNotificationsResults.class); + if(maxID!=null) + addQueryParameter("max_id", maxID); + if(limit>0) + addQueryParameter("limit", ""+limit); + if(includeTypes!=null){ + for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){ + addQueryParameter("types[]", type); + } + for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){ + addQueryParameter("exclude_types[]", type); + } + } + if(groupedTypes!=null){ + for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){ + addQueryParameter("grouped_types[]", type); + } + } + if(!TextUtils.isEmpty(onlyAccountID)) + addQueryParameter("account_id", onlyAccountID); + removeUnsupportedItems=true; + } + + @Override + protected String getPathPrefix(){ + return "/api/v2"; + } + + @AllFieldsAreRequired + public static class GroupedNotificationsResults extends BaseModel{ + public List accounts; + public List statuses; + public List notificationGroups; + + @Override + public void postprocess() throws ObjectValidationException{ + super.postprocess(); + for(Account acc:accounts) + acc.postprocess(); + for(Status s:statuses) + s.postprocess(); + for(NotificationGroup ng:notificationGroups) + ng.postprocess(); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetUnreadNotificationsCount.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetUnreadNotificationsCount.java new file mode 100644 index 000000000..e00f9654c --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetUnreadNotificationsCount.java @@ -0,0 +1,18 @@ +package org.joinmastodon.android.api.requests.notifications; + +import org.joinmastodon.android.api.MastodonAPIRequest; + +public class GetUnreadNotificationsCount extends MastodonAPIRequest{ + public GetUnreadNotificationsCount(){ + super(HttpMethod.GET, "/notifications/unread_count", Response.class); + } + + @Override + protected String getPathPrefix(){ + return "/api/v2"; + } + + public static class Response{ + public int count; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 396100d8a..4e2921ccb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -31,6 +31,7 @@ import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterResult; import org.joinmastodon.android.model.FollowList; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.PushSubscription; @@ -350,4 +351,8 @@ public class AccountSession{ public int getDonationSeed(){ return Math.abs(getFullUsername().hashCode())%100; } + + public Instance getInstanceInfo(){ + return AccountSessionManager.getInstance().getInstanceInfo(domain); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 15c73fdbe..66c3279e6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -455,6 +455,7 @@ public class AccountSessionManager{ .toString()); values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription)); values.put("flags", session.getFlagsForDatabase()); + values.put("push_id", session.pushAccountID); db.update("accounts", values, "`id`=?", new String[]{id}); }); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountNotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountNotificationsListFragment.java index 28a36aeb6..4336d45fe 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountNotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountNotificationsListFragment.java @@ -13,25 +13,22 @@ import android.widget.TextView; import org.joinmastodon.android.E; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest; import org.joinmastodon.android.events.NotificationRequestRespondedEvent; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.NotificationType; +import org.joinmastodon.android.model.viewmodel.NotificationViewModel; import org.joinmastodon.android.ui.Snackbar; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; -import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter; @@ -56,16 +53,16 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm protected void doLoadData(int offset, int count){ if(!refreshing && endMark!=null) endMark.setVisibility(View.GONE); - currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id) - .setCallback(new SimpleCallback<>(this){ - @Override - public void onSuccess(List result){ - onDataLoaded(result, !result.isEmpty()); - maxID=result.isEmpty() ? null : result.get(result.size()-1).id; - endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); - } - }) - .exec(accountID); +// currentRequest=new GetNotificationsV2(offset==0 ? null : maxID, count, EnumSet.allOf(NotificationType.class), account.id) +// .setCallback(new SimpleCallback<>(this){ +// @Override +// public void onSuccess(List result){ +// onDataLoaded(result, !result.isEmpty()); +// maxID=result.isEmpty() ? null : result.get(result.size()-1).id; +// endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); +// } +// }) +// .exec(accountID); } @Override @@ -153,8 +150,8 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm } @Override - protected List buildDisplayItems(Notification n){ - if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){ + protected List buildDisplayItems(NotificationViewModel n){ + if(n.notification.type==NotificationType.MENTION || n.notification.type==NotificationType.STATUS){ return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN); } return super.buildDisplayItems(n); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseNotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseNotificationsListFragment.java index ecaf63885..599d04686 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseNotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseNotificationsListFragment.java @@ -5,8 +5,10 @@ import android.view.LayoutInflater; import android.view.View; import org.joinmastodon.android.R; -import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.NotificationType; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.viewmodel.NotificationViewModel; import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -16,17 +18,17 @@ import java.util.List; import me.grishka.appkit.Nav; -public abstract class BaseNotificationsListFragment extends BaseStatusListFragment{ +public abstract class BaseNotificationsListFragment extends BaseStatusListFragment{ protected String maxID; protected View endMark; @Override - protected List buildDisplayItems(Notification n){ + protected List buildDisplayItems(NotificationViewModel n){ NotificationHeaderStatusDisplayItem titleItem; - if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){ + if(n.notification.type==NotificationType.MENTION || n.notification.type==NotificationType.STATUS){ titleItem=null; }else{ - titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID); + titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID); if(n.status!=null){ n.status.card=null; n.status.spoilerText=null; @@ -34,7 +36,7 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme } if(n.status!=null){ if(titleItem!=null){ - return List.of(titleItem, new InlineStatusStatusDisplayItem(n.id, this, n.status)); + return List.of(titleItem, new InlineStatusStatusDisplayItem(n.getID(), this, n.status)); }else{ return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, 0); } @@ -46,16 +48,18 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme } @Override - protected void addAccountToKnown(Notification s){ - if(!knownAccounts.containsKey(s.account.id)) - knownAccounts.put(s.account.id, s.account); + protected void addAccountToKnown(NotificationViewModel s){ + for(Account a:s.accounts){ + if(!knownAccounts.containsKey(a.id)) + knownAccounts.put(a.id, a); + } if(s.status!=null && !knownAccounts.containsKey(s.status.account.id)) knownAccounts.put(s.status.account.id, s.status.account); } @Override public void onItemClick(String id){ - Notification n=getNotificationByID(id); + NotificationViewModel n=getNotificationByID(id); if(n.status!=null){ Status status=n.status; Bundle args=new Bundle(); @@ -67,25 +71,25 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme }else{ Bundle args=new Bundle(); args.putString("account", accountID); - args.putParcelable("profileAccount", Parcels.wrap(n.account)); + args.putParcelable("profileAccount", Parcels.wrap(n.accounts.get(0))); Nav.go(getActivity(), ProfileFragment.class, args); } } - private Notification getNotificationByID(String id){ - for(Notification n : data){ - if(n.id.equals(id)) + protected NotificationViewModel getNotificationByID(String id){ + for(NotificationViewModel n:data){ + if(n.getID().equals(id)) return n; } return null; } - protected void removeNotification(Notification n){ + protected void removeNotification(NotificationViewModel n){ data.remove(n); preloadedData.remove(n); int index=-1; for(int i=0; i[] notifications=new List[]{null}; - String[] marker={null}; + if(AccountSessionManager.get(accountID).getInstanceInfo().getApiVersion()>=2){ + new GetUnreadNotificationsCount() + .setCallback(new Callback<>(){ + @Override + public void onSuccess(GetUnreadNotificationsCount.Response result){ + updateUnreadNotificationsBadge(result.count, false); + } - AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{ - marker[0]=m; - if(notifications[0]!=null){ - updateUnreadCount(notifications[0], marker[0]); - } - }); + @Override + public void onError(ErrorResponse error){ - AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, true, new Callback<>(){ - @Override - public void onSuccess(PaginatedResponse> result){ - notifications[0]=result.items; - if(marker[0]!=null) - updateUnreadCount(notifications[0], marker[0]); - } + } + }) + .exec(accountID); + }else{ + List[] notifications=new List[]{null}; + String[] marker={null}; + AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{ + marker[0]=m; + if(notifications[0]!=null){ + updateUnreadCountV1(notifications[0], marker[0]); + } + }); - @Override - public void onError(ErrorResponse error){} - }); + new GetNotificationsV1(null, 40, EnumSet.allOf(NotificationType.class)) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + notifications[0]=result; + if(marker[0]!=null) + updateUnreadCountV1(notifications[0], marker[0]); + } + + @Override + public void onError(ErrorResponse error){} + }).exec(accountID); + } } @SuppressLint("DefaultLocale") - private void updateUnreadCount(List notifications, String marker){ + private void updateUnreadCountV1(List notifications, String marker){ if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){ - notificationsBadge.setVisibility(View.GONE); + updateUnreadNotificationsBadge(0, false); }else{ - notificationsBadge.setVisibility(View.VISIBLE); if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){ - notificationsBadge.setText(String.format("%d+", notifications.size())); + updateUnreadNotificationsBadge(notifications.size(), true); }else{ int count=0; for(Notification n:notifications){ @@ -326,11 +344,20 @@ public class HomeFragment extends AppKitFragment{ break; count++; } - notificationsBadge.setText(String.format("%d", count)); + updateUnreadNotificationsBadge(count, false); } } } + private void updateUnreadNotificationsBadge(int count, boolean more){ + if(count==0){ + notificationsBadge.setVisibility(View.GONE); + }else{ + notificationsBadge.setVisibility(View.VISIBLE); + notificationsBadge.setText(String.format(more ? "%d+" : "%d", count)); + } + } + @Subscribe public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){ if(!ev.accountID.equals(accountID)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 3f902f3ad..b774a78aa 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -24,12 +24,12 @@ import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolic import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent; -import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.NotificationsPolicy; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.ListItem; +import org.joinmastodon.android.model.viewmodel.NotificationViewModel; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter; @@ -64,6 +64,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ private ArrayList> requestsItems=new ArrayList<>(); private GenericListItemsAdapter requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems); private NotificationsPolicy lastPolicy; + private boolean refreshAfterLoading; @Override public void onCreate(Bundle savedInstanceState){ @@ -96,13 +97,17 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ .getAccount(accountID).getCacheController() .getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){ @Override - public void onSuccess(PaginatedResponse> result){ + public void onSuccess(PaginatedResponse> result){ if(getActivity()==null) return; - onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty()); + onDataLoaded(result.items, !result.items.isEmpty()); maxID=result.maxID; endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE); reloadingFromCache=false; + if(refreshAfterLoading){ + refreshAfterLoading=false; + refresh(); + } } }); } @@ -111,9 +116,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ protected void onShown(){ super.onShown(); unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker(); - if(!dataLoading && canRefreshWithoutUpsettingUser()){ - reloadingFromCache=true; - refresh(); + if(canRefreshWithoutUpsettingUser()){ + if(dataLoading) + refreshAfterLoading=true; + else + refresh(); } } @@ -158,7 +165,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ for(int i=0;i holder){ - String itemID=holder.getItemID(); + String itemID=getNotificationByID(holder.getItemID()).notification.pageMaxId; if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){ parent.getDecoratedBoundsWithMargins(child, tmpRect); c.drawRect(tmpRect, paint); @@ -180,12 +187,12 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ public void onPollUpdated(PollUpdatedEvent ev){ if(!ev.accountID.equals(accountID)) return; - for(Notification ntf:data){ + for(NotificationViewModel ntf:data){ if(ntf.status==null) continue; Status contentStatus=ntf.status.getContentStatus(); if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){ - updatePoll(ntf.id, ntf.status, ev.poll); + updatePoll(ntf.getID(), ntf.status, ev.poll); } } } @@ -194,10 +201,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){ if(!ev.accountID.equals(accountID) || ev.isUnfollow) return; - List toRemove=Stream.concat(data.stream(), preloadedData.stream()) - .filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID)) + List toRemove=Stream.concat(data.stream(), preloadedData.stream()) + .filter(n->n.status!=null && n.status.account.id.equals(ev.postsByAccountID)) .collect(Collectors.toList()); - for(Notification n:toRemove){ + for(NotificationViewModel n:toRemove){ removeNotification(n); } } @@ -253,7 +260,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ private void markAsRead(){ if(data.isEmpty()) return; - String id=data.get(0).id; + String id=data.get(0).notification.pageMaxId; if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){ new SaveMarkers(null, id).exec(accountID); AccountSessionManager.get(accountID).setNotificationsMarker(id, true); @@ -276,12 +283,13 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ } @Override - public void onAppendItems(List items){ + public void onAppendItems(List items){ super.onAppendItems(items); - if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker)) + // TODO + if(data.isEmpty() || data.get(0).getID().equals(realUnreadMarker)) return; - for(Notification n:items){ - if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){ + for(NotificationViewModel n:items){ + if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){ markAsRead(); break; } @@ -296,7 +304,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{ if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder itemHolder){ String id=itemHolder.getItemID(); for(int j=0;j sampleAccountIds; + public String statusId; + // TODO report + // TODO event + // TODO moderation_warning +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/NotificationType.java b/mastodon/src/main/java/org/joinmastodon/android/model/NotificationType.java new file mode 100644 index 000000000..569596462 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/NotificationType.java @@ -0,0 +1,30 @@ +package org.joinmastodon.android.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.EnumSet; + +public enum NotificationType{ + @SerializedName("follow") + FOLLOW, + @SerializedName("follow_request") + FOLLOW_REQUEST, + @SerializedName("mention") + MENTION, + @SerializedName("reblog") + REBLOG, + @SerializedName("favourite") + FAVORITE, + @SerializedName("poll") + POLL, + @SerializedName("status") + STATUS; + + public boolean canBeGrouped(){ + return this==REBLOG || this==FAVORITE; + } + + public static EnumSet getGroupableTypes(){ + return EnumSet.of(FAVORITE, REBLOG); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/NotificationViewModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/NotificationViewModel.java new file mode 100644 index 000000000..0f54d8cf6 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/NotificationViewModel.java @@ -0,0 +1,19 @@ +package org.joinmastodon.android.model.viewmodel; + +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.DisplayItemsParent; +import org.joinmastodon.android.model.NotificationGroup; +import org.joinmastodon.android.model.Status; + +import java.util.List; + +public class NotificationViewModel implements DisplayItemsParent{ + public NotificationGroup notification; + public List accounts; + public Status status; + + @Override + public String getID(){ + return notification.groupKey; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java index 64beb83fa..b066997f2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java @@ -5,6 +5,7 @@ import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.SpannableStringBuilder; +import android.text.TextPaint; import android.text.TextUtils; import android.text.style.TypefaceSpan; import android.view.View; @@ -14,15 +15,23 @@ import android.widget.TextView; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.ProfileFragment; -import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.NotificationType; +import org.joinmastodon.android.model.viewmodel.NotificationViewModel; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.text.LinkSpan; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + import me.grishka.appkit.Nav; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; @@ -30,43 +39,76 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{ - public final Notification notification; - private ImageLoaderRequest avaRequest; + public final NotificationViewModel notification; private String accountID; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CharSequence text; + private List accounts; + private List avaRequests; - public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){ + public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, NotificationViewModel notification, String accountID){ super(parentID, parentFragment); this.notification=notification; this.accountID=accountID; - if(notification.type==Notification.Type.POLL){ - text=parentFragment.getString(R.string.poll_ended); + if(notification.accounts.size()<=6){ + accounts=notification.accounts; }else{ - avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50)); - SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName); - HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis); + accounts=notification.accounts.subList(0, 6); + } + avaRequests=accounts.stream() + .map(a->new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? a.avatar : a.avatarStatic, V.dp(50), V.dp(50))) + .collect(Collectors.toList()); + + if(notification.notification.type==NotificationType.POLL && AccountSessionManager.getInstance().isSelf(accountID, notification.accounts.get(0))){ + text=parentFragment.getString(R.string.own_poll_ended); + }else{ + Account account=notification.accounts.get(0); + SpannableStringBuilder parsedName=new SpannableStringBuilder(account.displayName); + HtmlParser.parseCustomEmoji(parsedName, account.emojis); emojiHelper.setText(parsedName); - String[] parts=parentFragment.getString(switch(notification.type){ - case FOLLOW -> R.string.user_followed_you; - case FOLLOW_REQUEST -> R.string.user_sent_follow_request; - case REBLOG -> R.string.notification_boosted; - case FAVORITE -> R.string.user_favorited; - default -> throw new IllegalStateException("Unexpected value: "+notification.type); - }).split("%s", 2); - SpannableStringBuilder text=new SpannableStringBuilder(); - if(parts.length>1 && !TextUtils.isEmpty(parts[0])) - text.append(parts[0]); - text.append(parsedName, new TypefaceSpan("sans-serif-medium"), 0); - if(parts.length==1){ - text.append(' '); - text.append(parts[0]); - }else if(!TextUtils.isEmpty(parts[1])){ - text.append(parts[1]); + String text; + if(accounts.size()>1 && notification.notification.type.canBeGrouped()){ + text=parentFragment.getResources().getQuantityString(switch(notification.notification.type){ + case FAVORITE -> R.plurals.user_and_x_more_favorited; + case REBLOG -> R.plurals.user_and_x_more_boosted; + default -> throw new IllegalStateException("Unexpected value: " + notification.notification.type); + }, notification.notification.notificationsCount-1, "{{name}}", notification.notification.notificationsCount-1); + }else if(notification.notification.type==NotificationType.POLL){ + if(notification.status==null || notification.status.poll==null){ + text="???"; + }else{ + int count=notification.status.poll.votersCount-1; + text=parentFragment.getResources().getQuantityString(R.plurals.poll_ended_x_voters, count, "{{name}}", count); + } + }else{ + text=parentFragment.getString(switch(notification.notification.type){ + case FOLLOW -> R.string.user_followed_you; + case FOLLOW_REQUEST -> R.string.user_sent_follow_request; + case REBLOG -> R.string.notification_boosted; + case FAVORITE -> R.string.user_favorited; + default -> throw new IllegalStateException("Unexpected value: "+notification.notification.type); + }, "{{name}}"); } - this.text=text; + String[] parts=text.split(Pattern.quote("{{name}}"), 2); + SpannableStringBuilder formattedText=new SpannableStringBuilder(); + if(parts.length>1 && !TextUtils.isEmpty(parts[0])) + formattedText.append(parts[0]); + formattedText.append(parsedName, new TypefaceSpan("sans-serif-medium"), 0); + formattedText.setSpan(new NonColoredLinkSpan(null, s->{ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("profileAccount", Parcels.wrap(account)); + Nav.go(parentFragment.getActivity(), ProfileFragment.class, args); + }, LinkSpan.Type.CUSTOM, null, null, null), formattedText.length()-parsedName.length(), formattedText.length(), 0); + if(parts.length==1){ + formattedText.append(' '); + formattedText.append(parts[0]); + }else if(!TextUtils.isEmpty(parts[1])){ + formattedText.append(parts[1]); + } + this.text=formattedText; } } @@ -77,46 +119,61 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{ @Override public int getImageCount(){ - return 1+emojiHelper.getImageCount(); + return avaRequests.size()+emojiHelper.getImageCount(); } @Override public ImageLoaderRequest getImageRequest(int index){ - if(index>0){ - return emojiHelper.getImageRequest(index-1); + if(index>=avaRequests.size()){ + return emojiHelper.getImageRequest(index-avaRequests.size()); } - return avaRequest; + return avaRequests.get(index); } public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - private final ImageView icon, avatar; + private final ImageView icon; private final TextView text; + private final ImageView[] avatars; + private final View avatarsContainer; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_notification_header, parent); icon=findViewById(R.id.icon); - avatar=findViewById(R.id.avatar); text=findViewById(R.id.text); + avatars=new ImageView[]{ + findViewById(R.id.avatar1), + findViewById(R.id.avatar2), + findViewById(R.id.avatar3), + findViewById(R.id.avatar4), + findViewById(R.id.avatar5), + findViewById(R.id.avatar6), + }; + avatarsContainer=findViewById(R.id.avatars); - avatar.setOutlineProvider(OutlineProviders.roundedRect(8)); - avatar.setClipToOutline(true); - avatar.setOnClickListener(this::onAvaClick); + int i=0; + for(ImageView avatar:avatars){ + avatar.setOutlineProvider(OutlineProviders.roundedRect(6)); + avatar.setClipToOutline(true); + avatar.setOnClickListener(this::onAvaClick); + avatar.setTag(i); + i++; + } } @Override public void setImage(int index, Drawable image){ - if(index==0){ - avatar.setImageDrawable(image); + if(index=item.accounts.size()){ + avatars[i].setVisibility(View.GONE); + }else{ + avatars[i].setVisibility(View.VISIBLE); + avatars[i].setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.accounts.get(i).acct)); + } + } + } + icon.setImageResource(switch(item.notification.notification.type){ case FAVORITE -> R.drawable.ic_star_fill1_24px; case REBLOG -> R.drawable.ic_repeat_fill1_24px; case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_person_add_fill1_24px; case POLL -> R.drawable.ic_insert_chart_fill1_24px; - default -> throw new IllegalStateException("Unexpected value: "+item.notification.type); + default -> throw new IllegalStateException("Unexpected value: "+item.notification.notification.type); }); - icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){ + icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.notification.type){ case FAVORITE -> R.attr.colorFavorite; case REBLOG -> R.attr.colorBoost; - case FOLLOW, FOLLOW_REQUEST -> R.attr.colorFollow; - case POLL -> R.attr.colorPoll; - default -> throw new IllegalStateException("Unexpected value: "+item.notification.type); + case FOLLOW, FOLLOW_REQUEST -> R.attr.colorM3Primary; + default -> R.attr.colorM3Outline; }))); + itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.notification.status==null ? V.dp(12) : 0); } private void onAvaClick(View v){ Bundle args=new Bundle(); args.putString("account", item.accountID); - args.putParcelable("profileAccount", Parcels.wrap(item.notification.account)); + args.putParcelable("profileAccount", Parcels.wrap(item.notification.accounts.get((Integer)v.getTag()))); Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args); } } + + private static class NonColoredLinkSpan extends LinkSpan{ + public NonColoredLinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, Object linkObject, Object parentObject){ + super(link, listener, type, accountID, linkObject, parentObject); + } + + @Override + public void updateDrawState(TextPaint tp){ + color=tp.getColor(); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index c11e707c8..1b5a66db7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -12,7 +12,7 @@ import org.joinmastodon.android.ui.utils.UiUtils; public class LinkSpan extends CharacterStyle { - private int color=0xFF00FF00; + protected int color=0xFF00FF00; private OnLinkClickListener listener; private String link; private Type type; diff --git a/mastodon/src/main/res/layout/display_item_notification_header.xml b/mastodon/src/main/res/layout/display_item_notification_header.xml index fa5f8a4f3..6d0918e78 100644 --- a/mastodon/src/main/res/layout/display_item_notification_header.xml +++ b/mastodon/src/main/res/layout/display_item_notification_header.xml @@ -1,34 +1,74 @@ - + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:paddingTop="12dp"> - - - + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index a81b290d8..960bce24c 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -33,8 +33,6 @@ - - diff --git a/mastodon/src/main/res/values/colors.xml b/mastodon/src/main/res/values/colors.xml index d2c99cc8e..01eb175ca 100644 --- a/mastodon/src/main/res/values/colors.xml +++ b/mastodon/src/main/res/values/colors.xml @@ -54,5 +54,55 @@ #938F99 #49454F + + #E89A00 + #7B5800 + #CC9200 + #FFB014 + #FFFFFF + #FFFFFF + #FFFFFF + #412D00 + #FFC758 + #FFC758 + #FFC758 + #F9B928 + #503800 + #503800 + #503800 + #463100 + #006C4E + #006C4E + #006C4E + #48DEAB + #FFFFFF + #FFFFFF + #FFFFFF + #003827 + #25C896 + #25C896 + #25C896 + #00B384 + #002C1E + #002C1E + #002C1E + #00130B + #A2003E + #A2003E + #A2003E + #FFB2BD + #FFFFFF + #FFFFFF + #FFFFFF + #670024 + #DF235E + #DF235E + #DF235E + #D31656 + #FFFFFF + #FFFFFF + #FFFFFF + #FFFFFF + 0.12 \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index b4485591e..bb5c308b4 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -16,9 +16,8 @@ %s followed you %s sent you a follow request - %s favorited your post - %s boosted your post - See the results of a poll you voted in + %s favorited: + %s boosted: Share Settings @@ -784,4 +783,17 @@ %d attachment %d attachments + + %1$s and %2$,d other favorited: + %1$s and %2$,d others favorited: + + + %1$s and %2$,d other boosted: + %1$s and %2$,d others boosted: + + + %1$s ran a poll that you and %2$,d other voted in + %1$s ran a poll that you and %2$,d others voted in + + Your poll has ended \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index e664e9d15..7db971633 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -56,10 +56,8 @@ @color/m3_sys_dark_surface @color/m3_sys_dark_on_surface #FFF - #8b5000 - #ab332a - #4746e3 - #006d42 + @color/ext_favorite_light + @color/ext_boost_light ?colorM3Background @color/navigation_bar_bg_light @@ -126,10 +124,8 @@ @color/m3_sys_light_surface @color/m3_sys_light_on_surface #000 - #ffb871 - #ffb4aa - #c1c1ff - #77daa1 + @color/ext_favorite_dark + @color/ext_boost_dark ?colorM3Background ?colorM3Background