Grouped notifications
This commit is contained in:
parent
80323f8236
commit
26f7a75628
23 changed files with 758 additions and 251 deletions
|
@ -23,6 +23,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Mention;
|
import org.joinmastodon.android.model.Mention;
|
||||||
|
import org.joinmastodon.android.model.NotificationType;
|
||||||
import org.joinmastodon.android.model.PushNotification;
|
import org.joinmastodon.android.model.PushNotification;
|
||||||
import org.joinmastodon.android.model.StatusPrivacy;
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -183,7 +184,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||||
builder.setSubText(accountName);
|
builder.setSubText(accountName);
|
||||||
}
|
}
|
||||||
String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
|
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<String> mentions=new ArrayList<>();
|
ArrayList<String> mentions=new ArrayList<>();
|
||||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||||
if(!notification.status.account.id.equals(ownID))
|
if(!notification.status.account.id.equals(ownID))
|
||||||
|
|
|
@ -14,26 +14,35 @@ import com.google.gson.reflect.TypeToken;
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
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.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.FollowList;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Notification;
|
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.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.SearchResult;
|
import org.joinmastodon.android.model.SearchResult;
|
||||||
import org.joinmastodon.android.model.Status;
|
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.IOException;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
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.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
@ -41,7 +50,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||||
|
|
||||||
public class CacheController{
|
public class CacheController{
|
||||||
private static final String TAG="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 WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||||
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@ -49,7 +58,7 @@ public class CacheController{
|
||||||
private DatabaseHelper db;
|
private DatabaseHelper db;
|
||||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||||
private boolean loadingNotifications;
|
private boolean loadingNotifications;
|
||||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
private final ArrayList<Callback<PaginatedResponse<List<NotificationViewModel>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||||
private List<FollowList> lists;
|
private List<FollowList> lists;
|
||||||
|
|
||||||
private static final int POST_FLAG_GAP_AFTER=1;
|
private static final int POST_FLAG_GAP_AFTER=1;
|
||||||
|
@ -133,53 +142,106 @@ public class CacheController{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
private List<NotificationViewModel> makeNotificationViewModels(List<NotificationGroup> notifications, Map<String, Account> accounts, Map<String, Status> 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<PaginatedResponse<List<NotificationViewModel>>> callback){
|
||||||
cancelDelayedClose();
|
cancelDelayedClose();
|
||||||
databaseThread.postRunnable(()->{
|
databaseThread.postRunnable(()->{
|
||||||
try{
|
try{
|
||||||
if(!onlyMentions && loadingNotifications){
|
|
||||||
synchronized(pendingNotificationsCallbacks){
|
|
||||||
pendingNotificationsCallbacks.add(callback);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
String suffix=onlyMentions ? "mentions" : "all";
|
||||||
|
String table="notifications_"+suffix;
|
||||||
|
String accountsTable="notifications_accounts_"+suffix;
|
||||||
|
String statusesTable="notifications_statuses_"+suffix;
|
||||||
|
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`max_id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||||
if(cursor.getCount()==count){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Notification> result=new ArrayList<>();
|
ArrayList<NotificationGroup> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
String newMaxID;
|
String newMaxID;
|
||||||
|
HashSet<String> needAccounts=new HashSet<>(), needStatuses=new HashSet<>();
|
||||||
do{
|
do{
|
||||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
NotificationGroup ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), NotificationGroup.class);
|
||||||
ntf.postprocess();
|
ntf.postprocess();
|
||||||
newMaxID=ntf.id;
|
newMaxID=ntf.pageMinId;
|
||||||
|
needAccounts.addAll(ntf.sampleAccountIds);
|
||||||
|
if(ntf.statusId!=null)
|
||||||
|
needStatuses.add(ntf.statusId);
|
||||||
result.add(ntf);
|
result.add(ntf);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
HashMap<String, Account> accounts=new HashMap<>();
|
||||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
HashMap<String, Status> 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;
|
return;
|
||||||
}
|
}
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!onlyMentions && loadingNotifications){
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
pendingNotificationsCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!onlyMentions)
|
if(!onlyMentions)
|
||||||
loadingNotifications=true;
|
loadingNotifications=true;
|
||||||
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
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<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Notification> result){
|
public void onSuccess(GetNotificationsV2.GroupedNotificationsResults result){
|
||||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
Map<String, Account> accounts=result.accounts.stream().collect(Collectors.toMap(a->a.id, Function.identity(), (a1, a2)->a2));
|
||||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
Map<String, Status> statuses=result.statuses.stream().collect(Collectors.toMap(s->s.id, Function.identity(), (s1, s2)->s2));
|
||||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
List<NotificationViewModel> notifications=makeNotificationViewModels(result.notificationGroups, accounts, statuses);
|
||||||
|
databaseThread.postRunnable(()->putNotifications(result.notificationGroups, result.accounts, result.statuses, onlyMentions, maxID==null), 0);
|
||||||
|
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(notifications,
|
||||||
|
result.notificationGroups.isEmpty() ? null : result.notificationGroups.get(result.notificationGroups.size()-1).pageMinId);
|
||||||
callback.onSuccess(res);
|
callback.onSuccess(res);
|
||||||
putNotifications(result, onlyMentions, maxID==null);
|
|
||||||
if(!onlyMentions){
|
if(!onlyMentions){
|
||||||
loadingNotifications=false;
|
loadingNotifications=false;
|
||||||
synchronized(pendingNotificationsCallbacks){
|
synchronized(pendingNotificationsCallbacks){
|
||||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||||
cb.onSuccess(res);
|
cb.onSuccess(res);
|
||||||
}
|
}
|
||||||
pendingNotificationsCallbacks.clear();
|
pendingNotificationsCallbacks.clear();
|
||||||
|
@ -193,7 +255,7 @@ public class CacheController{
|
||||||
if(!onlyMentions){
|
if(!onlyMentions){
|
||||||
loadingNotifications=false;
|
loadingNotifications=false;
|
||||||
synchronized(pendingNotificationsCallbacks){
|
synchronized(pendingNotificationsCallbacks){
|
||||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||||
cb.onError(error);
|
cb.onError(error);
|
||||||
}
|
}
|
||||||
pendingNotificationsCallbacks.clear();
|
pendingNotificationsCallbacks.clear();
|
||||||
|
@ -202,6 +264,54 @@ public class CacheController{
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.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<Notification> result){
|
||||||
|
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||||
|
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||||
|
List<Status> statuses=filtered.stream().map(n->n.status).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
List<Account> accounts=filtered.stream().map(n->n.account).collect(Collectors.toList());
|
||||||
|
List<NotificationViewModel> 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<List<NotificationViewModel>> 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<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||||
|
cb.onError(error);
|
||||||
|
}
|
||||||
|
pendingNotificationsCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
}catch(SQLiteException x){
|
}catch(SQLiteException x){
|
||||||
Log.w(TAG, x);
|
Log.w(TAG, x);
|
||||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||||
|
@ -211,22 +321,40 @@ public class CacheController{
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
|
private void putNotifications(List<NotificationGroup> notifications, List<Account> accounts, List<Status> statuses, boolean onlyMentions, boolean clear){
|
||||||
runOnDbThread((db)->{
|
runOnDbThread((db)->{
|
||||||
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
String suffix=onlyMentions ? "mentions" : "all";
|
||||||
if(clear)
|
String table="notifications_"+suffix;
|
||||||
|
String accountsTable="notifications_accounts_"+suffix;
|
||||||
|
String statusesTable="notifications_statuses_"+suffix;
|
||||||
|
if(clear){
|
||||||
db.delete(table, null, null);
|
db.delete(table, null, null);
|
||||||
|
db.delete(accountsTable, null, null);
|
||||||
|
db.delete(statusesTable, null, null);
|
||||||
|
}
|
||||||
ContentValues values=new ContentValues(4);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Notification n:notifications){
|
for(NotificationGroup n:notifications){
|
||||||
if(n.type==null){
|
if(n.type==null){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
values.put("id", n.id);
|
values.put("id", n.groupKey);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||||
values.put("type", n.type.ordinal());
|
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);
|
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,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`time` INTEGER NOT NULL
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
createNotificationsTables(db, "all");
|
||||||
CREATE TABLE `notifications_all` (
|
createNotificationsTables(db, "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
|
|
||||||
)""");
|
|
||||||
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
|
|
||||||
)""");
|
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
createMiscTable(db);
|
createMiscTable(db);
|
||||||
}
|
}
|
||||||
|
@ -440,6 +554,12 @@ public class CacheController{
|
||||||
if(oldVersion<4){
|
if(oldVersion<4){
|
||||||
createMiscTable(db);
|
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){
|
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||||
|
@ -467,5 +587,28 @@ public class CacheController{
|
||||||
`value` TEXT
|
`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
|
||||||
|
)""");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,26 +7,27 @@ import com.google.gson.reflect.TypeToken;
|
||||||
import org.joinmastodon.android.api.ApiUtils;
|
import org.joinmastodon.android.api.ApiUtils;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
|
import org.joinmastodon.android.model.NotificationType;
|
||||||
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
public class GetNotificationsV1 extends MastodonAPIRequest<List<Notification>>{
|
||||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
|
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes){
|
||||||
this(maxID, limit, includeTypes, null);
|
this(maxID, limit, includeTypes, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
|
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes, String onlyAccountID){
|
||||||
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
||||||
if(maxID!=null)
|
if(maxID!=null)
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", ""+limit);
|
addQueryParameter("limit", ""+limit);
|
||||||
if(includeTypes!=null){
|
if(includeTypes!=null){
|
||||||
for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){
|
for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
|
||||||
addQueryParameter("types[]", type);
|
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);
|
addQueryParameter("exclude_types[]", type);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<GetNotificationsV2.GroupedNotificationsResults>{
|
||||||
|
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
|
||||||
|
this(maxID, limit, includeTypes, groupedTypes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> 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<Account> accounts;
|
||||||
|
public List<Status> statuses;
|
||||||
|
public List<NotificationGroup> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
|
||||||
|
public class GetUnreadNotificationsCount extends MastodonAPIRequest<GetUnreadNotificationsCount.Response>{
|
||||||
|
public GetUnreadNotificationsCount(){
|
||||||
|
super(HttpMethod.GET, "/notifications/unread_count", Response.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix(){
|
||||||
|
return "/api/v2";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response{
|
||||||
|
public int count;
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import org.joinmastodon.android.model.FilterAction;
|
||||||
import org.joinmastodon.android.model.FilterContext;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
import org.joinmastodon.android.model.FilterResult;
|
import org.joinmastodon.android.model.FilterResult;
|
||||||
import org.joinmastodon.android.model.FollowList;
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.LegacyFilter;
|
import org.joinmastodon.android.model.LegacyFilter;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
|
@ -350,4 +351,8 @@ public class AccountSession{
|
||||||
public int getDonationSeed(){
|
public int getDonationSeed(){
|
||||||
return Math.abs(getFullUsername().hashCode())%100;
|
return Math.abs(getFullUsername().hashCode())%100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Instance getInstanceInfo(){
|
||||||
|
return AccountSessionManager.getInstance().getInstanceInfo(domain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -455,6 +455,7 @@ public class AccountSessionManager{
|
||||||
.toString());
|
.toString());
|
||||||
values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription));
|
values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription));
|
||||||
values.put("flags", session.getFlagsForDatabase());
|
values.put("flags", session.getFlagsForDatabase());
|
||||||
|
values.put("push_id", session.pushAccountID);
|
||||||
db.update("accounts", values, "`id`=?", new String[]{id});
|
db.update("accounts", values, "`id`=?", new String[]{id});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,25 +13,22 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
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.api.requests.notifications.RespondToNotificationRequest;
|
||||||
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
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.Snackbar;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
|
||||||
|
@ -56,16 +53,16 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
if(!refreshing && endMark!=null)
|
if(!refreshing && endMark!=null)
|
||||||
endMark.setVisibility(View.GONE);
|
endMark.setVisibility(View.GONE);
|
||||||
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
|
// currentRequest=new GetNotificationsV2(offset==0 ? null : maxID, count, EnumSet.allOf(NotificationType.class), account.id)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
// .setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
// @Override
|
||||||
public void onSuccess(List<Notification> result){
|
// public void onSuccess(List<NotificationViewModel> result){
|
||||||
onDataLoaded(result, !result.isEmpty());
|
// onDataLoaded(result, !result.isEmpty());
|
||||||
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
// maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||||
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
// endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.exec(accountID);
|
// .exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -153,8 +150,8 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
|
||||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
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 StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||||
}
|
}
|
||||||
return super.buildDisplayItems(n);
|
return super.buildDisplayItems(n);
|
||||||
|
|
|
@ -5,8 +5,10 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
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.Status;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
|
||||||
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
@ -16,17 +18,17 @@ import java.util.List;
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
|
|
||||||
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
|
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<NotificationViewModel>{
|
||||||
protected String maxID;
|
protected String maxID;
|
||||||
protected View endMark;
|
protected View endMark;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
|
||||||
NotificationHeaderStatusDisplayItem titleItem;
|
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;
|
titleItem=null;
|
||||||
}else{
|
}else{
|
||||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID);
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
n.status.card=null;
|
n.status.card=null;
|
||||||
n.status.spoilerText=null;
|
n.status.spoilerText=null;
|
||||||
|
@ -34,7 +36,7 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||||
}
|
}
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
if(titleItem!=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{
|
}else{
|
||||||
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, 0);
|
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, 0);
|
||||||
}
|
}
|
||||||
|
@ -46,16 +48,18 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addAccountToKnown(Notification s){
|
protected void addAccountToKnown(NotificationViewModel s){
|
||||||
if(!knownAccounts.containsKey(s.account.id))
|
for(Account a:s.accounts){
|
||||||
knownAccounts.put(s.account.id, s.account);
|
if(!knownAccounts.containsKey(a.id))
|
||||||
|
knownAccounts.put(a.id, a);
|
||||||
|
}
|
||||||
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
||||||
knownAccounts.put(s.status.account.id, s.status.account);
|
knownAccounts.put(s.status.account.id, s.status.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(String id){
|
public void onItemClick(String id){
|
||||||
Notification n=getNotificationByID(id);
|
NotificationViewModel n=getNotificationByID(id);
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
Status status=n.status;
|
Status status=n.status;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
|
@ -67,25 +71,25 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||||
}else{
|
}else{
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
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);
|
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Notification getNotificationByID(String id){
|
protected NotificationViewModel getNotificationByID(String id){
|
||||||
for(Notification n : data){
|
for(NotificationViewModel n:data){
|
||||||
if(n.id.equals(id))
|
if(n.getID().equals(id))
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void removeNotification(Notification n){
|
protected void removeNotification(NotificationViewModel n){
|
||||||
data.remove(n);
|
data.remove(n);
|
||||||
preloadedData.remove(n);
|
preloadedData.remove(n);
|
||||||
int index=-1;
|
int index=-1;
|
||||||
for(int i=0; i<displayItems.size(); i++){
|
for(int i=0; i<displayItems.size(); i++){
|
||||||
if(n.id.equals(displayItems.get(i).parentID)){
|
if(n.getID().equals(displayItems.get(i).parentID)){
|
||||||
index=i;
|
index=i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +98,7 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||||
return;
|
return;
|
||||||
int lastIndex;
|
int lastIndex;
|
||||||
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
|
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
|
||||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
if(!displayItems.get(lastIndex).parentID.equals(n.getID()))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
displayItems.subList(index, lastIndex).clear();
|
displayItems.subList(index, lastIndex).clear();
|
||||||
|
|
|
@ -21,6 +21,8 @@ import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.PushNotificationReceiver;
|
import org.joinmastodon.android.PushNotificationReceiver;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV1;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetUnreadNotificationsCount;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||||
|
@ -29,7 +31,7 @@ import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.NotificationType;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -38,6 +40,7 @@ import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
|
@ -288,37 +291,52 @@ public class HomeFragment extends AppKitFragment{
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadNotificationsForUnreadCount(){
|
private void reloadNotificationsForUnreadCount(){
|
||||||
|
if(AccountSessionManager.get(accountID).getInstanceInfo().getApiVersion()>=2){
|
||||||
|
new GetUnreadNotificationsCount()
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(GetUnreadNotificationsCount.Response result){
|
||||||
|
updateUnreadNotificationsBadge(result.count, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}else{
|
||||||
List<Notification>[] notifications=new List[]{null};
|
List<Notification>[] notifications=new List[]{null};
|
||||||
String[] marker={null};
|
String[] marker={null};
|
||||||
|
|
||||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||||
marker[0]=m;
|
marker[0]=m;
|
||||||
if(notifications[0]!=null){
|
if(notifications[0]!=null){
|
||||||
updateUnreadCount(notifications[0], marker[0]);
|
updateUnreadCountV1(notifications[0], marker[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, true, new Callback<>(){
|
new GetNotificationsV1(null, 40, EnumSet.allOf(NotificationType.class))
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
public void onSuccess(List<Notification> result){
|
||||||
notifications[0]=result.items;
|
notifications[0]=result;
|
||||||
if(marker[0]!=null)
|
if(marker[0]!=null)
|
||||||
updateUnreadCount(notifications[0], marker[0]);
|
updateUnreadCountV1(notifications[0], marker[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){}
|
public void onError(ErrorResponse error){}
|
||||||
});
|
}).exec(accountID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
private void updateUnreadCount(List<Notification> notifications, String marker){
|
private void updateUnreadCountV1(List<Notification> notifications, String marker){
|
||||||
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
|
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
|
||||||
notificationsBadge.setVisibility(View.GONE);
|
updateUnreadNotificationsBadge(0, false);
|
||||||
}else{
|
}else{
|
||||||
notificationsBadge.setVisibility(View.VISIBLE);
|
|
||||||
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
|
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
|
||||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
updateUnreadNotificationsBadge(notifications.size(), true);
|
||||||
}else{
|
}else{
|
||||||
int count=0;
|
int count=0;
|
||||||
for(Notification n:notifications){
|
for(Notification n:notifications){
|
||||||
|
@ -326,11 +344,20 @@ public class HomeFragment extends AppKitFragment{
|
||||||
break;
|
break;
|
||||||
count++;
|
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
|
@Subscribe
|
||||||
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
|
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
|
||||||
if(!ev.accountID.equals(accountID))
|
if(!ev.accountID.equals(accountID))
|
||||||
|
|
|
@ -24,12 +24,12 @@ import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolic
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.model.Notification;
|
|
||||||
import org.joinmastodon.android.model.NotificationsPolicy;
|
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
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.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||||
|
@ -64,6 +64,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
|
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
|
||||||
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
|
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
|
||||||
private NotificationsPolicy lastPolicy;
|
private NotificationsPolicy lastPolicy;
|
||||||
|
private boolean refreshAfterLoading;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
@ -96,13 +97,17 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
.getAccount(accountID).getCacheController()
|
.getAccount(accountID).getCacheController()
|
||||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
public void onSuccess(PaginatedResponse<List<NotificationViewModel>> result){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
return;
|
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;
|
maxID=result.maxID;
|
||||||
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
|
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
reloadingFromCache=false;
|
reloadingFromCache=false;
|
||||||
|
if(refreshAfterLoading){
|
||||||
|
refreshAfterLoading=false;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -111,8 +116,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
protected void onShown(){
|
protected void onShown(){
|
||||||
super.onShown();
|
super.onShown();
|
||||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
if(canRefreshWithoutUpsettingUser()){
|
||||||
reloadingFromCache=true;
|
if(dataLoading)
|
||||||
|
refreshAfterLoading=true;
|
||||||
|
else
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +165,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
for(int i=0;i<parent.getChildCount();i++){
|
for(int i=0;i<parent.getChildCount();i++){
|
||||||
View child=parent.getChildAt(i);
|
View child=parent.getChildAt(i);
|
||||||
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||||
String itemID=holder.getItemID();
|
String itemID=getNotificationByID(holder.getItemID()).notification.pageMaxId;
|
||||||
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
||||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||||
c.drawRect(tmpRect, paint);
|
c.drawRect(tmpRect, paint);
|
||||||
|
@ -180,12 +187,12 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
public void onPollUpdated(PollUpdatedEvent ev){
|
public void onPollUpdated(PollUpdatedEvent ev){
|
||||||
if(!ev.accountID.equals(accountID))
|
if(!ev.accountID.equals(accountID))
|
||||||
return;
|
return;
|
||||||
for(Notification ntf:data){
|
for(NotificationViewModel ntf:data){
|
||||||
if(ntf.status==null)
|
if(ntf.status==null)
|
||||||
continue;
|
continue;
|
||||||
Status contentStatus=ntf.status.getContentStatus();
|
Status contentStatus=ntf.status.getContentStatus();
|
||||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
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){
|
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||||
return;
|
return;
|
||||||
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
List<NotificationViewModel> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||||
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
|
.filter(n->n.status!=null && n.status.account.id.equals(ev.postsByAccountID))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
for(Notification n:toRemove){
|
for(NotificationViewModel n:toRemove){
|
||||||
removeNotification(n);
|
removeNotification(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +260,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
private void markAsRead(){
|
private void markAsRead(){
|
||||||
if(data.isEmpty())
|
if(data.isEmpty())
|
||||||
return;
|
return;
|
||||||
String id=data.get(0).id;
|
String id=data.get(0).notification.pageMaxId;
|
||||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||||
new SaveMarkers(null, id).exec(accountID);
|
new SaveMarkers(null, id).exec(accountID);
|
||||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||||
|
@ -276,12 +283,13 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAppendItems(List<Notification> items){
|
public void onAppendItems(List<NotificationViewModel> items){
|
||||||
super.onAppendItems(items);
|
super.onAppendItems(items);
|
||||||
if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker))
|
// TODO
|
||||||
|
if(data.isEmpty() || data.get(0).getID().equals(realUnreadMarker))
|
||||||
return;
|
return;
|
||||||
for(Notification n:items){
|
for(NotificationViewModel n:items){
|
||||||
if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){
|
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
|
||||||
markAsRead();
|
markAsRead();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -296,7 +304,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
||||||
String id=itemHolder.getItemID();
|
String id=itemHolder.getItemID();
|
||||||
for(int j=0;j<data.size();j++){
|
for(int j=0;j<data.size();j++){
|
||||||
if(data.get(j).id.equals(id))
|
if(data.get(j).getID().equals(id))
|
||||||
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,10 @@ public abstract class Instance extends BaseModel{
|
||||||
public abstract int getVersion();
|
public abstract int getVersion();
|
||||||
public abstract long getApiVersion(String name);
|
public abstract long getApiVersion(String name);
|
||||||
|
|
||||||
|
public long getApiVersion(){
|
||||||
|
return getApiVersion("mastodon");
|
||||||
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class Rule{
|
public static class Rule{
|
||||||
public String id;
|
public String id;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
|
@ -13,7 +11,7 @@ public class Notification extends BaseModel implements DisplayItemsParent{
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String id;
|
public String id;
|
||||||
// @RequiredField
|
// @RequiredField
|
||||||
public Type type;
|
public NotificationType type;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public Instant createdAt;
|
public Instant createdAt;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
|
@ -38,21 +36,4 @@ public class Notification extends BaseModel implements DisplayItemsParent{
|
||||||
public String getAccountID(){
|
public String getAccountID(){
|
||||||
return status!=null ? account.id : null;
|
return status!=null ? account.id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type{
|
|
||||||
@SerializedName("follow")
|
|
||||||
FOLLOW,
|
|
||||||
@SerializedName("follow_request")
|
|
||||||
FOLLOW_REQUEST,
|
|
||||||
@SerializedName("mention")
|
|
||||||
MENTION,
|
|
||||||
@SerializedName("reblog")
|
|
||||||
REBLOG,
|
|
||||||
@SerializedName("favourite")
|
|
||||||
FAVORITE,
|
|
||||||
@SerializedName("poll")
|
|
||||||
POLL,
|
|
||||||
@SerializedName("status")
|
|
||||||
STATUS
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class NotificationGroup extends BaseModel{
|
||||||
|
@RequiredField
|
||||||
|
public String groupKey;
|
||||||
|
public int notificationsCount;
|
||||||
|
public NotificationType type;
|
||||||
|
@RequiredField
|
||||||
|
public String mostRecentNotificationId;
|
||||||
|
public String pageMinId;
|
||||||
|
public String pageMaxId;
|
||||||
|
public Instant latestPageNotificationAt;
|
||||||
|
@RequiredField
|
||||||
|
public List<String> sampleAccountIds;
|
||||||
|
public String statusId;
|
||||||
|
// TODO report
|
||||||
|
// TODO event
|
||||||
|
// TODO moderation_warning
|
||||||
|
}
|
|
@ -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<NotificationType> getGroupableTypes(){
|
||||||
|
return EnumSet.of(FAVORITE, REBLOG);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Account> accounts;
|
||||||
|
public Status status;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getID(){
|
||||||
|
return notification.groupKey;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import android.content.res.ColorStateList;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextPaint;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -14,15 +15,23 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
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.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
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.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
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.Nav;
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
|
@ -30,43 +39,76 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
public final Notification notification;
|
public final NotificationViewModel notification;
|
||||||
private ImageLoaderRequest avaRequest;
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||||
private CharSequence text;
|
private CharSequence text;
|
||||||
|
private List<Account> accounts;
|
||||||
|
private List<ImageLoaderRequest> avaRequests;
|
||||||
|
|
||||||
public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){
|
public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, NotificationViewModel notification, String accountID){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.notification=notification;
|
this.notification=notification;
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
|
|
||||||
if(notification.type==Notification.Type.POLL){
|
if(notification.accounts.size()<=6){
|
||||||
text=parentFragment.getString(R.string.poll_ended);
|
accounts=notification.accounts;
|
||||||
}else{
|
}else{
|
||||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50));
|
accounts=notification.accounts.subList(0, 6);
|
||||||
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName);
|
}
|
||||||
HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis);
|
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);
|
emojiHelper.setText(parsedName);
|
||||||
|
|
||||||
String[] parts=parentFragment.getString(switch(notification.type){
|
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 -> R.string.user_followed_you;
|
||||||
case FOLLOW_REQUEST -> R.string.user_sent_follow_request;
|
case FOLLOW_REQUEST -> R.string.user_sent_follow_request;
|
||||||
case REBLOG -> R.string.notification_boosted;
|
case REBLOG -> R.string.notification_boosted;
|
||||||
case FAVORITE -> R.string.user_favorited;
|
case FAVORITE -> R.string.user_favorited;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+notification.type);
|
default -> throw new IllegalStateException("Unexpected value: "+notification.notification.type);
|
||||||
}).split("%s", 2);
|
}, "{{name}}");
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
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
|
@Override
|
||||||
public int getImageCount(){
|
public int getImageCount(){
|
||||||
return 1+emojiHelper.getImageCount();
|
return avaRequests.size()+emojiHelper.getImageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImageLoaderRequest getImageRequest(int index){
|
public ImageLoaderRequest getImageRequest(int index){
|
||||||
if(index>0){
|
if(index>=avaRequests.size()){
|
||||||
return emojiHelper.getImageRequest(index-1);
|
return emojiHelper.getImageRequest(index-avaRequests.size());
|
||||||
}
|
}
|
||||||
return avaRequest;
|
return avaRequests.get(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final ImageView icon, avatar;
|
private final ImageView icon;
|
||||||
private final TextView text;
|
private final TextView text;
|
||||||
|
private final ImageView[] avatars;
|
||||||
|
private final View avatarsContainer;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_notification_header, parent);
|
super(activity, R.layout.display_item_notification_header, parent);
|
||||||
icon=findViewById(R.id.icon);
|
icon=findViewById(R.id.icon);
|
||||||
avatar=findViewById(R.id.avatar);
|
|
||||||
text=findViewById(R.id.text);
|
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));
|
int i=0;
|
||||||
|
for(ImageView avatar:avatars){
|
||||||
|
avatar.setOutlineProvider(OutlineProviders.roundedRect(6));
|
||||||
avatar.setClipToOutline(true);
|
avatar.setClipToOutline(true);
|
||||||
avatar.setOnClickListener(this::onAvaClick);
|
avatar.setOnClickListener(this::onAvaClick);
|
||||||
|
avatar.setTag(i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setImage(int index, Drawable image){
|
public void setImage(int index, Drawable image){
|
||||||
if(index==0){
|
if(index<item.avaRequests.size()){
|
||||||
avatar.setImageDrawable(image);
|
avatars[index].setImageDrawable(image);
|
||||||
}else{
|
}else{
|
||||||
item.emojiHelper.setImageDrawable(index-1, image);
|
item.emojiHelper.setImageDrawable(index-item.avaRequests.size(), image);
|
||||||
text.invalidate();
|
text.invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearImage(int index){
|
public void clearImage(int index){
|
||||||
if(index==0)
|
if(index<item.avaRequests.size())
|
||||||
avatar.setImageResource(R.drawable.image_placeholder);
|
avatars[index].setImageResource(R.drawable.image_placeholder);
|
||||||
else
|
else
|
||||||
ImageLoaderViewHolder.super.clearImage(index);
|
ImageLoaderViewHolder.super.clearImage(index);
|
||||||
}
|
}
|
||||||
|
@ -124,30 +181,52 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
@Override
|
@Override
|
||||||
public void onBind(NotificationHeaderStatusDisplayItem item){
|
public void onBind(NotificationHeaderStatusDisplayItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
|
// avatar.setVisibility(item.notification.notification.type==NotificationType.POLL ? View.GONE : View.VISIBLE);
|
||||||
if(item.notification.type!=Notification.Type.POLL)
|
if(item.notification.notification.type==NotificationType.POLL){
|
||||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.account.acct));
|
avatarsContainer.setVisibility(View.GONE);
|
||||||
icon.setImageResource(switch(item.notification.type){
|
}else{
|
||||||
|
avatarsContainer.setVisibility(View.VISIBLE);
|
||||||
|
for(int i=0;i<avatars.length;i++){
|
||||||
|
if(i>=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 FAVORITE -> R.drawable.ic_star_fill1_24px;
|
||||||
case REBLOG -> R.drawable.ic_repeat_fill1_24px;
|
case REBLOG -> R.drawable.ic_repeat_fill1_24px;
|
||||||
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_person_add_fill1_24px;
|
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_person_add_fill1_24px;
|
||||||
case POLL -> R.drawable.ic_insert_chart_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 FAVORITE -> R.attr.colorFavorite;
|
||||||
case REBLOG -> R.attr.colorBoost;
|
case REBLOG -> R.attr.colorBoost;
|
||||||
case FOLLOW, FOLLOW_REQUEST -> R.attr.colorFollow;
|
case FOLLOW, FOLLOW_REQUEST -> R.attr.colorM3Primary;
|
||||||
case POLL -> R.attr.colorPoll;
|
default -> R.attr.colorM3Outline;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+item.notification.type);
|
|
||||||
})));
|
})));
|
||||||
|
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.notification.status==null ? V.dp(12) : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAvaClick(View v){
|
private void onAvaClick(View v){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", item.accountID);
|
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);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
public class LinkSpan extends CharacterStyle {
|
public class LinkSpan extends CharacterStyle {
|
||||||
|
|
||||||
private int color=0xFF00FF00;
|
protected int color=0xFF00FF00;
|
||||||
private OnLinkClickListener listener;
|
private OnLinkClickListener listener;
|
||||||
private String link;
|
private String link;
|
||||||
private Type type;
|
private Type type;
|
||||||
|
|
|
@ -1,34 +1,74 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingHorizontal="16dp">
|
android:paddingTop="12dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
android:layout_width="28dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="28dp"
|
android:layout_height="28dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="center"
|
||||||
tools:tint="#0f0"
|
tools:tint="#0f0"
|
||||||
tools:src="@drawable/ic_repeat_24px"/>
|
tools:src="@drawable/ic_repeat_24px"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/avatars"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_toEndOf="@id/icon"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar1"
|
||||||
android:layout_width="32dp"
|
android:layout_width="28dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="28dp"
|
||||||
android:layout_marginStart="8dp"/>
|
android:layout_marginEnd="8dp"/>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar2"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar3"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar4"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar5"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar6"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<org.joinmastodon.android.ui.views.LinkedTextView
|
||||||
android:id="@+id/text"
|
android:id="@+id/text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_toEndOf="@id/icon"
|
||||||
android:textAppearance="@style/m3_body_large"
|
android:layout_below="@id/avatars"
|
||||||
|
android:layout_alignWithParentIfMissing="true"
|
||||||
|
android:textAppearance="@style/m3_body_medium"
|
||||||
android:textColor="?colorM3OnSurface"
|
android:textColor="?colorM3OnSurface"
|
||||||
android:singleLine="true"
|
android:minHeight="20dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
tools:text="Notification text"/>
|
tools:text="Notification text"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</RelativeLayout>
|
|
@ -33,8 +33,6 @@
|
||||||
<attr name="colorWhite" format="color"/>
|
<attr name="colorWhite" format="color"/>
|
||||||
<attr name="colorFavorite" format="color" />
|
<attr name="colorFavorite" format="color" />
|
||||||
<attr name="colorBoost" format="color" />
|
<attr name="colorBoost" format="color" />
|
||||||
<attr name="colorFollow" format="color" />
|
|
||||||
<attr name="colorPoll" format="color" />
|
|
||||||
|
|
||||||
<declare-styleable name="MaxWidthFrameLayout">
|
<declare-styleable name="MaxWidthFrameLayout">
|
||||||
<attr name="android:maxWidth" format="dimension"/>
|
<attr name="android:maxWidth" format="dimension"/>
|
||||||
|
|
|
@ -54,5 +54,55 @@
|
||||||
<color name="m3_sys_dark_outline">#938F99</color>
|
<color name="m3_sys_dark_outline">#938F99</color>
|
||||||
<color name="m3_sys_dark_outline_variant">#49454F</color>
|
<color name="m3_sys_dark_outline_variant">#49454F</color>
|
||||||
|
|
||||||
|
<!-- extended colors -->
|
||||||
|
<color name="ext_favorite_light">#E89A00</color>
|
||||||
|
<color name="ext_favorite_light_high_contrast">#7B5800</color>
|
||||||
|
<color name="ext_favorite_light_medium_contrast">#CC9200</color>
|
||||||
|
<color name="ext_favorite_dark">#FFB014</color>
|
||||||
|
<color name="ext_on_favorite_light">#FFFFFF</color>
|
||||||
|
<color name="ext_on_favorite_light_high_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_favorite_light_medium_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_favorite_dark">#412D00</color>
|
||||||
|
<color name="ext_favorite_container_light">#FFC758</color>
|
||||||
|
<color name="ext_favorite_container_light_high_contrast">#FFC758</color>
|
||||||
|
<color name="ext_favorite_container_light_medium_contrast">#FFC758</color>
|
||||||
|
<color name="ext_favorite_container_dark">#F9B928</color>
|
||||||
|
<color name="ext_on_favorite_container_light">#503800</color>
|
||||||
|
<color name="ext_on_favorite_container_light_high_contrast">#503800</color>
|
||||||
|
<color name="ext_on_favorite_container_light_medium_contrast">#503800</color>
|
||||||
|
<color name="ext_on_favorite_container_dark">#463100</color>
|
||||||
|
<color name="ext_boost_light">#006C4E</color>
|
||||||
|
<color name="ext_boost_light_high_contrast">#006C4E</color>
|
||||||
|
<color name="ext_boost_light_medium_contrast">#006C4E</color>
|
||||||
|
<color name="ext_boost_dark">#48DEAB</color>
|
||||||
|
<color name="ext_on_boost_light">#FFFFFF</color>
|
||||||
|
<color name="ext_on_boost_light_high_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_boost_light_medium_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_boost_dark">#003827</color>
|
||||||
|
<color name="ext_boost_container_light">#25C896</color>
|
||||||
|
<color name="ext_boost_container_light_high_contrast">#25C896</color>
|
||||||
|
<color name="ext_boost_container_light_medium_contrast">#25C896</color>
|
||||||
|
<color name="ext_boost_container_dark">#00B384</color>
|
||||||
|
<color name="ext_on_boost_container_light">#002C1E</color>
|
||||||
|
<color name="ext_on_boost_container_light_high_contrast">#002C1E</color>
|
||||||
|
<color name="ext_on_boost_container_light_medium_contrast">#002C1E</color>
|
||||||
|
<color name="ext_on_boost_container_dark">#00130B</color>
|
||||||
|
<color name="ext_bookmark_light">#A2003E</color>
|
||||||
|
<color name="ext_bookmark_light_high_contrast">#A2003E</color>
|
||||||
|
<color name="ext_bookmark_light_medium_contrast">#A2003E</color>
|
||||||
|
<color name="ext_bookmark_dark">#FFB2BD</color>
|
||||||
|
<color name="ext_on_bookmark_light">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_light_high_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_light_medium_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_dark">#670024</color>
|
||||||
|
<color name="ext_bookmark_container_light">#DF235E</color>
|
||||||
|
<color name="ext_bookmark_container_light_high_contrast">#DF235E</color>
|
||||||
|
<color name="ext_bookmark_container_light_medium_contrast">#DF235E</color>
|
||||||
|
<color name="ext_bookmark_container_dark">#D31656</color>
|
||||||
|
<color name="ext_on_bookmark_container_light">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_container_light_high_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_container_light_medium_contrast">#FFFFFF</color>
|
||||||
|
<color name="ext_on_bookmark_container_dark">#FFFFFF</color>
|
||||||
|
|
||||||
<item name="overlay_ripple_alpha" format="float" type="dimen">0.12</item>
|
<item name="overlay_ripple_alpha" format="float" type="dimen">0.12</item>
|
||||||
</resources>
|
</resources>
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
<string name="user_followed_you">%s followed you</string>
|
<string name="user_followed_you">%s followed you</string>
|
||||||
<string name="user_sent_follow_request">%s sent you a follow request</string>
|
<string name="user_sent_follow_request">%s sent you a follow request</string>
|
||||||
<string name="user_favorited">%s favorited your post</string>
|
<string name="user_favorited">%s favorited:</string>
|
||||||
<string name="notification_boosted">%s boosted your post</string>
|
<string name="notification_boosted">%s boosted:</string>
|
||||||
<string name="poll_ended">See the results of a poll you voted in</string>
|
|
||||||
|
|
||||||
<string name="share_toot_title">Share</string>
|
<string name="share_toot_title">Share</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
@ -784,4 +783,17 @@
|
||||||
<item quantity="one">%d attachment</item>
|
<item quantity="one">%d attachment</item>
|
||||||
<item quantity="other">%d attachments</item>
|
<item quantity="other">%d attachments</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="user_and_x_more_favorited">
|
||||||
|
<item quantity="one">%1$s and %2$,d other favorited:</item>
|
||||||
|
<item quantity="other">%1$s and %2$,d others favorited:</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="user_and_x_more_boosted">
|
||||||
|
<item quantity="one">%1$s and %2$,d other boosted:</item>
|
||||||
|
<item quantity="other">%1$s and %2$,d others boosted:</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="poll_ended_x_voters">
|
||||||
|
<item quantity="one">%1$s ran a poll that you and %2$,d other voted in</item>
|
||||||
|
<item quantity="other">%1$s ran a poll that you and %2$,d others voted in</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="own_poll_ended">Your poll has ended</string>
|
||||||
</resources>
|
</resources>
|
|
@ -56,10 +56,8 @@
|
||||||
<item name="colorM3SurfaceInverse">@color/m3_sys_dark_surface</item>
|
<item name="colorM3SurfaceInverse">@color/m3_sys_dark_surface</item>
|
||||||
<item name="colorM3OnSurfaceInverse">@color/m3_sys_dark_on_surface</item>
|
<item name="colorM3OnSurfaceInverse">@color/m3_sys_dark_on_surface</item>
|
||||||
<item name="colorWhite">#FFF</item>
|
<item name="colorWhite">#FFF</item>
|
||||||
<item name="colorFavorite">#8b5000</item>
|
<item name="colorFavorite">@color/ext_favorite_light</item>
|
||||||
<item name="colorBoost">#ab332a</item>
|
<item name="colorBoost">@color/ext_boost_light</item>
|
||||||
<item name="colorFollow">#4746e3</item>
|
|
||||||
<item name="colorPoll">#006d42</item>
|
|
||||||
|
|
||||||
<item name="android:statusBarColor">?colorM3Background</item>
|
<item name="android:statusBarColor">?colorM3Background</item>
|
||||||
<item name="android:navigationBarColor">@color/navigation_bar_bg_light</item>
|
<item name="android:navigationBarColor">@color/navigation_bar_bg_light</item>
|
||||||
|
@ -126,10 +124,8 @@
|
||||||
<item name="colorM3SurfaceInverse">@color/m3_sys_light_surface</item>
|
<item name="colorM3SurfaceInverse">@color/m3_sys_light_surface</item>
|
||||||
<item name="colorM3OnSurfaceInverse">@color/m3_sys_light_on_surface</item>
|
<item name="colorM3OnSurfaceInverse">@color/m3_sys_light_on_surface</item>
|
||||||
<item name="colorWhite">#000</item>
|
<item name="colorWhite">#000</item>
|
||||||
<item name="colorFavorite">#ffb871</item>
|
<item name="colorFavorite">@color/ext_favorite_dark</item>
|
||||||
<item name="colorBoost">#ffb4aa</item>
|
<item name="colorBoost">@color/ext_boost_dark</item>
|
||||||
<item name="colorFollow">#c1c1ff</item>
|
|
||||||
<item name="colorPoll">#77daa1</item>
|
|
||||||
|
|
||||||
<item name="android:statusBarColor">?colorM3Background</item>
|
<item name="android:statusBarColor">?colorM3Background</item>
|
||||||
<item name="android:navigationBarColor">?colorM3Background</item>
|
<item name="android:navigationBarColor">?colorM3Background</item>
|
||||||
|
|
Loading…
Add table
Reference in a new issue