Add notifications for follow requests (#1729)
* Add notifications for follow requests Issue #1719 * Revert item_follow_request layout, create new layout for follow request notifications * Migrate follow request interaction from notification to observable pattern * Filter follow request notifications by default * Add missing cases for system notification generation * Format code
This commit is contained in:
		
					parent
					
						
							
								43162789c1
							
						
					
				
			
			
				commit
				
					
						4a4dd4f30f
					
				
			
		
					 16 changed files with 1003 additions and 104 deletions
				
			
		
							
								
								
									
										735
									
								
								app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										735
									
								
								app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,735 @@ | |||
| { | ||||
|   "formatVersion": 1, | ||||
|   "database": { | ||||
|     "version": 22, | ||||
|     "identityHash": "eaa3c4d012fe743948343983fe1ae493", | ||||
|     "entities": [ | ||||
|       { | ||||
|         "tableName": "TootEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "uid", | ||||
|             "columnName": "uid", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "text", | ||||
|             "columnName": "text", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "urls", | ||||
|             "columnName": "urls", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "descriptions", | ||||
|             "columnName": "descriptions", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "contentWarning", | ||||
|             "columnName": "contentWarning", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "inReplyToId", | ||||
|             "columnName": "inReplyToId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "inReplyToText", | ||||
|             "columnName": "inReplyToText", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "inReplyToUsername", | ||||
|             "columnName": "inReplyToUsername", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "visibility", | ||||
|             "columnName": "visibility", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "poll", | ||||
|             "columnName": "poll", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "uid" | ||||
|           ], | ||||
|           "autoGenerate": true | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "AccountEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "domain", | ||||
|             "columnName": "domain", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "accessToken", | ||||
|             "columnName": "accessToken", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "isActive", | ||||
|             "columnName": "isActive", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "accountId", | ||||
|             "columnName": "accountId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "username", | ||||
|             "columnName": "username", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "displayName", | ||||
|             "columnName": "displayName", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "profilePictureUrl", | ||||
|             "columnName": "profilePictureUrl", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsEnabled", | ||||
|             "columnName": "notificationsEnabled", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsMentioned", | ||||
|             "columnName": "notificationsMentioned", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsFollowed", | ||||
|             "columnName": "notificationsFollowed", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsFollowRequested", | ||||
|             "columnName": "notificationsFollowRequested", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsReblogged", | ||||
|             "columnName": "notificationsReblogged", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsFavorited", | ||||
|             "columnName": "notificationsFavorited", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsPolls", | ||||
|             "columnName": "notificationsPolls", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationSound", | ||||
|             "columnName": "notificationSound", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationVibration", | ||||
|             "columnName": "notificationVibration", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationLight", | ||||
|             "columnName": "notificationLight", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "defaultPostPrivacy", | ||||
|             "columnName": "defaultPostPrivacy", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "defaultMediaSensitivity", | ||||
|             "columnName": "defaultMediaSensitivity", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "alwaysShowSensitiveMedia", | ||||
|             "columnName": "alwaysShowSensitiveMedia", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "alwaysOpenSpoiler", | ||||
|             "columnName": "alwaysOpenSpoiler", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "mediaPreviewEnabled", | ||||
|             "columnName": "mediaPreviewEnabled", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastNotificationId", | ||||
|             "columnName": "lastNotificationId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "activeNotifications", | ||||
|             "columnName": "activeNotifications", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "emojis", | ||||
|             "columnName": "emojis", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "tabPreferences", | ||||
|             "columnName": "tabPreferences", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "notificationsFilter", | ||||
|             "columnName": "notificationsFilter", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id" | ||||
|           ], | ||||
|           "autoGenerate": true | ||||
|         }, | ||||
|         "indices": [ | ||||
|           { | ||||
|             "name": "index_AccountEntity_domain_accountId", | ||||
|             "unique": true, | ||||
|             "columnNames": [ | ||||
|               "domain", | ||||
|               "accountId" | ||||
|             ], | ||||
|             "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" | ||||
|           } | ||||
|         ], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "InstanceEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "instance", | ||||
|             "columnName": "instance", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "emojiList", | ||||
|             "columnName": "emojiList", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "maximumTootCharacters", | ||||
|             "columnName": "maximumTootCharacters", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "maxPollOptions", | ||||
|             "columnName": "maxPollOptions", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "maxPollOptionLength", | ||||
|             "columnName": "maxPollOptionLength", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "version", | ||||
|             "columnName": "version", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "instance" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "TimelineStatusEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "serverId", | ||||
|             "columnName": "serverId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "url", | ||||
|             "columnName": "url", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "timelineUserId", | ||||
|             "columnName": "timelineUserId", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "authorServerId", | ||||
|             "columnName": "authorServerId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "inReplyToId", | ||||
|             "columnName": "inReplyToId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "inReplyToAccountId", | ||||
|             "columnName": "inReplyToAccountId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "content", | ||||
|             "columnName": "content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "createdAt", | ||||
|             "columnName": "createdAt", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "emojis", | ||||
|             "columnName": "emojis", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "reblogsCount", | ||||
|             "columnName": "reblogsCount", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "favouritesCount", | ||||
|             "columnName": "favouritesCount", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "reblogged", | ||||
|             "columnName": "reblogged", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "bookmarked", | ||||
|             "columnName": "bookmarked", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "favourited", | ||||
|             "columnName": "favourited", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "sensitive", | ||||
|             "columnName": "sensitive", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "spoilerText", | ||||
|             "columnName": "spoilerText", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "visibility", | ||||
|             "columnName": "visibility", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "attachments", | ||||
|             "columnName": "attachments", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "mentions", | ||||
|             "columnName": "mentions", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "application", | ||||
|             "columnName": "application", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "reblogServerId", | ||||
|             "columnName": "reblogServerId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "reblogAccountId", | ||||
|             "columnName": "reblogAccountId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "poll", | ||||
|             "columnName": "poll", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "serverId", | ||||
|             "timelineUserId" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [ | ||||
|           { | ||||
|             "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", | ||||
|             "unique": false, | ||||
|             "columnNames": [ | ||||
|               "authorServerId", | ||||
|               "timelineUserId" | ||||
|             ], | ||||
|             "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" | ||||
|           } | ||||
|         ], | ||||
|         "foreignKeys": [ | ||||
|           { | ||||
|             "table": "TimelineAccountEntity", | ||||
|             "onDelete": "NO ACTION", | ||||
|             "onUpdate": "NO ACTION", | ||||
|             "columns": [ | ||||
|               "authorServerId", | ||||
|               "timelineUserId" | ||||
|             ], | ||||
|             "referencedColumns": [ | ||||
|               "serverId", | ||||
|               "timelineUserId" | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "TimelineAccountEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "serverId", | ||||
|             "columnName": "serverId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "timelineUserId", | ||||
|             "columnName": "timelineUserId", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "localUsername", | ||||
|             "columnName": "localUsername", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "username", | ||||
|             "columnName": "username", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "displayName", | ||||
|             "columnName": "displayName", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "url", | ||||
|             "columnName": "url", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "avatar", | ||||
|             "columnName": "avatar", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "emojis", | ||||
|             "columnName": "emojis", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "bot", | ||||
|             "columnName": "bot", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "serverId", | ||||
|             "timelineUserId" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       }, | ||||
|       { | ||||
|         "tableName": "ConversationEntity", | ||||
|         "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", | ||||
|         "fields": [ | ||||
|           { | ||||
|             "fieldPath": "accountId", | ||||
|             "columnName": "accountId", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "id", | ||||
|             "columnName": "id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "accounts", | ||||
|             "columnName": "accounts", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "unread", | ||||
|             "columnName": "unread", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.id", | ||||
|             "columnName": "s_id", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.url", | ||||
|             "columnName": "s_url", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.inReplyToId", | ||||
|             "columnName": "s_inReplyToId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.inReplyToAccountId", | ||||
|             "columnName": "s_inReplyToAccountId", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.account", | ||||
|             "columnName": "s_account", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.content", | ||||
|             "columnName": "s_content", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.createdAt", | ||||
|             "columnName": "s_createdAt", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.emojis", | ||||
|             "columnName": "s_emojis", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.favouritesCount", | ||||
|             "columnName": "s_favouritesCount", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.favourited", | ||||
|             "columnName": "s_favourited", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.bookmarked", | ||||
|             "columnName": "s_bookmarked", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.sensitive", | ||||
|             "columnName": "s_sensitive", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.spoilerText", | ||||
|             "columnName": "s_spoilerText", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.attachments", | ||||
|             "columnName": "s_attachments", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.mentions", | ||||
|             "columnName": "s_mentions", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.showingHiddenContent", | ||||
|             "columnName": "s_showingHiddenContent", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.expanded", | ||||
|             "columnName": "s_expanded", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.collapsible", | ||||
|             "columnName": "s_collapsible", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.collapsed", | ||||
|             "columnName": "s_collapsed", | ||||
|             "affinity": "INTEGER", | ||||
|             "notNull": true | ||||
|           }, | ||||
|           { | ||||
|             "fieldPath": "lastStatus.poll", | ||||
|             "columnName": "s_poll", | ||||
|             "affinity": "TEXT", | ||||
|             "notNull": false | ||||
|           } | ||||
|         ], | ||||
|         "primaryKey": { | ||||
|           "columnNames": [ | ||||
|             "id", | ||||
|             "accountId" | ||||
|           ], | ||||
|           "autoGenerate": false | ||||
|         }, | ||||
|         "indices": [], | ||||
|         "foreignKeys": [] | ||||
|       } | ||||
|     ], | ||||
|     "views": [], | ||||
|     "setupQueries": [ | ||||
|       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", | ||||
|       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eaa3c4d012fe743948343983fe1ae493')" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,51 @@ | |||
| package com.keylesspalace.tusky.adapter | ||||
| 
 | ||||
| import android.view.View | ||||
| import androidx.core.text.BidiFormatter | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.keylesspalace.tusky.R | ||||
| import com.keylesspalace.tusky.entity.Account | ||||
| import com.keylesspalace.tusky.interfaces.AccountActionListener | ||||
| import com.keylesspalace.tusky.util.CustomEmojiHelper | ||||
| import com.keylesspalace.tusky.util.loadAvatar | ||||
| import com.keylesspalace.tusky.util.visible | ||||
| import kotlinx.android.synthetic.main.item_follow_request_notification.view.* | ||||
| 
 | ||||
| internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { | ||||
|     private var id: String? = null | ||||
|     private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context) | ||||
|             .getBoolean("animateGifAvatars", false) | ||||
| 
 | ||||
|     fun setupWithAccount(account: Account, formatter: BidiFormatter?) { | ||||
|         id = account.id | ||||
|         val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name | ||||
|         val emojifiedName: CharSequence = CustomEmojiHelper.emojifyString(wrappedName, account.emojis, itemView) | ||||
|         itemView.displayNameTextView.text = emojifiedName | ||||
|         if (showHeader) { | ||||
|             itemView.notificationTextView.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName) | ||||
|         } | ||||
|         itemView.notificationTextView.visible(showHeader) | ||||
|         val format = itemView.context.getString(R.string.status_username_format) | ||||
|         val formattedUsername = String.format(format, account.username) | ||||
|         itemView.usernameTextView.text = formattedUsername | ||||
|         val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) | ||||
|         loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar) | ||||
|     } | ||||
| 
 | ||||
|     fun setupActionListener(listener: AccountActionListener) { | ||||
|         itemView.acceptButton.setOnClickListener { | ||||
|             val position = adapterPosition | ||||
|             if (position != RecyclerView.NO_POSITION) { | ||||
|                 listener.onRespondToFollowRequest(true, id, position) | ||||
|             } | ||||
|         } | ||||
|         itemView.rejectButton.setOnClickListener { | ||||
|             val position = adapterPosition | ||||
|             if (position != RecyclerView.NO_POSITION) { | ||||
|                 listener.onRespondToFollowRequest(false, id, position) | ||||
|             } | ||||
|         } | ||||
|         itemView.avatar.setOnClickListener { listener.onViewAccount(id) } | ||||
|     } | ||||
| } | ||||
|  | @ -18,19 +18,12 @@ package com.keylesspalace.tusky.adapter; | |||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.preference.PreferenceManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| 
 | ||||
| import com.keylesspalace.tusky.R; | ||||
| import com.keylesspalace.tusky.entity.Account; | ||||
| import com.keylesspalace.tusky.interfaces.AccountActionListener; | ||||
| import com.keylesspalace.tusky.util.CustomEmojiHelper; | ||||
| import com.keylesspalace.tusky.util.ImageLoadingHelper; | ||||
| 
 | ||||
| public class FollowRequestsAdapter extends AccountAdapter { | ||||
| 
 | ||||
|  | @ -46,7 +39,7 @@ public class FollowRequestsAdapter extends AccountAdapter { | |||
|             case VIEW_TYPE_ACCOUNT: { | ||||
|                 View view = LayoutInflater.from(parent.getContext()) | ||||
|                         .inflate(R.layout.item_follow_request, parent, false); | ||||
|                 return new FollowRequestViewHolder(view); | ||||
|                 return new FollowRequestViewHolder(view, false); | ||||
|             } | ||||
|             case VIEW_TYPE_FOOTER: { | ||||
|                 View view = LayoutInflater.from(parent.getContext()) | ||||
|  | @ -60,57 +53,8 @@ public class FollowRequestsAdapter extends AccountAdapter { | |||
|     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { | ||||
|         if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { | ||||
|             FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; | ||||
|             holder.setupWithAccount(accountList.get(position)); | ||||
|             holder.setupWithAccount(accountList.get(position), null); | ||||
|             holder.setupActionListener(accountActionListener); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static class FollowRequestViewHolder extends RecyclerView.ViewHolder { | ||||
|         private ImageView avatar; | ||||
|         private TextView username; | ||||
|         private TextView displayName; | ||||
|         private ImageButton accept; | ||||
|         private ImageButton reject; | ||||
|         private String id; | ||||
|         private boolean animateAvatar; | ||||
| 
 | ||||
|         FollowRequestViewHolder(View itemView) { | ||||
|             super(itemView); | ||||
|             avatar = itemView.findViewById(R.id.avatar); | ||||
|             username = itemView.findViewById(R.id.usernameTextView); | ||||
|             displayName = itemView.findViewById(R.id.displayNameTextView); | ||||
|             accept = itemView.findViewById(R.id.acceptButton); | ||||
|             reject = itemView.findViewById(R.id.rejectButton); | ||||
|             animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) | ||||
|                     .getBoolean("animateGifAvatars", false); | ||||
|         } | ||||
| 
 | ||||
|         void setupWithAccount(Account account) { | ||||
|             id = account.getId(); | ||||
|             CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), account.getEmojis(), displayName); | ||||
|             displayName.setText(emojifiedName); | ||||
|             String format = username.getContext().getString(R.string.status_username_format); | ||||
|             String formattedUsername = String.format(format, account.getUsername()); | ||||
|             username.setText(formattedUsername); | ||||
|             int avatarRadius = avatar.getContext().getResources() | ||||
|                     .getDimensionPixelSize(R.dimen.avatar_radius_48dp); | ||||
|             ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); | ||||
|         } | ||||
| 
 | ||||
|         void setupActionListener(final AccountActionListener listener) { | ||||
|             accept.setOnClickListener(v -> { | ||||
|                 int position = getAdapterPosition(); | ||||
|                 if (position != RecyclerView.NO_POSITION) { | ||||
|                     listener.onRespondToFollowRequest(true, id, position); | ||||
|                 } | ||||
|             }); | ||||
|             reject.setOnClickListener(v -> { | ||||
|                 int position = getAdapterPosition(); | ||||
|                 if (position != RecyclerView.NO_POSITION) { | ||||
|                     listener.onRespondToFollowRequest(false, id, position); | ||||
|                 } | ||||
|             }); | ||||
|             avatar.setOnClickListener(v -> listener.onViewAccount(id)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ import com.keylesspalace.tusky.R; | |||
| import com.keylesspalace.tusky.entity.Account; | ||||
| import com.keylesspalace.tusky.entity.Emoji; | ||||
| import com.keylesspalace.tusky.entity.Notification; | ||||
| import com.keylesspalace.tusky.interfaces.AccountActionListener; | ||||
| import com.keylesspalace.tusky.interfaces.LinkListener; | ||||
| import com.keylesspalace.tusky.interfaces.StatusActionListener; | ||||
| import com.keylesspalace.tusky.util.CardViewMode; | ||||
|  | @ -72,8 +73,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|     private static final int VIEW_TYPE_STATUS = 0; | ||||
|     private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1; | ||||
|     private static final int VIEW_TYPE_FOLLOW = 2; | ||||
|     private static final int VIEW_TYPE_PLACEHOLDER = 3; | ||||
|     private static final int VIEW_TYPE_UNKNOWN = 4; | ||||
|     private static final int VIEW_TYPE_FOLLOW_REQUEST = 3; | ||||
|     private static final int VIEW_TYPE_PLACEHOLDER = 4; | ||||
|     private static final int VIEW_TYPE_UNKNOWN = 5; | ||||
| 
 | ||||
|     private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; | ||||
|     private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; | ||||
|  | @ -82,6 +84,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|     private StatusDisplayOptions statusDisplayOptions; | ||||
|     private StatusActionListener statusListener; | ||||
|     private NotificationActionListener notificationActionListener; | ||||
|     private AccountActionListener accountActionListener; | ||||
|     private BidiFormatter bidiFormatter; | ||||
|     private AdapterDataSource<NotificationViewData> dataSource; | ||||
| 
 | ||||
|  | @ -89,13 +92,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|                                 AdapterDataSource<NotificationViewData> dataSource, | ||||
|                                 StatusDisplayOptions statusDisplayOptions, | ||||
|                                 StatusActionListener statusListener, | ||||
|                                 NotificationActionListener notificationActionListener) { | ||||
|                                 NotificationActionListener notificationActionListener, | ||||
|                                 AccountActionListener accountActionListener) { | ||||
| 
 | ||||
|         this.accountId = accountId; | ||||
|         this.dataSource = dataSource; | ||||
|         this.statusDisplayOptions = statusDisplayOptions; | ||||
|         this.statusListener = statusListener; | ||||
|         this.notificationActionListener = notificationActionListener; | ||||
|         this.accountActionListener = accountActionListener; | ||||
|         bidiFormatter = BidiFormatter.getInstance(); | ||||
|     } | ||||
| 
 | ||||
|  | @ -119,6 +124,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|                         .inflate(R.layout.item_follow, parent, false); | ||||
|                 return new FollowViewHolder(view, statusDisplayOptions); | ||||
|             } | ||||
|             case VIEW_TYPE_FOLLOW_REQUEST: { | ||||
|                 View view = inflater | ||||
|                         .inflate(R.layout.item_follow_request_notification, parent, false); | ||||
|                 return new FollowRequestViewHolder(view, true); | ||||
|             } | ||||
|             case VIEW_TYPE_PLACEHOLDER: { | ||||
|                 View view = inflater | ||||
|                         .inflate(R.layout.item_status_placeholder, parent, false); | ||||
|  | @ -215,6 +225,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|                     } | ||||
|                     break; | ||||
|                 } | ||||
|                 case VIEW_TYPE_FOLLOW_REQUEST: { | ||||
|                     if (payloadForHolder == null) { | ||||
|                         FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; | ||||
|                         holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter); | ||||
|                         holder.setupActionListener(accountActionListener); | ||||
|                     } | ||||
|                 } | ||||
|                 default: | ||||
|             } | ||||
|         } | ||||
|  | @ -258,6 +275,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { | |||
|                 case FOLLOW: { | ||||
|                     return VIEW_TYPE_FOLLOW; | ||||
|                 } | ||||
|                 case FOLLOW_REQUEST: { | ||||
|                     return VIEW_TYPE_FOLLOW_REQUEST; | ||||
|                 } | ||||
|                 default: { | ||||
|                     return VIEW_TYPE_UNKNOWN; | ||||
|                 } | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, | |||
|                          var notificationsEnabled: Boolean = true, | ||||
|                          var notificationsMentioned: Boolean = true, | ||||
|                          var notificationsFollowed: Boolean = true, | ||||
|                          var notificationsFollowRequested: Boolean = false, | ||||
|                          var notificationsReblogged: Boolean = true, | ||||
|                          var notificationsFavorited: Boolean = true, | ||||
|                          var notificationsPolls: Boolean = true, | ||||
|  | @ -54,7 +55,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, | |||
|                          var activeNotifications: String = "[]", | ||||
|                          var emojis: List<Emoji> = emptyList(), | ||||
|                          var tabPreferences: List<TabData> = defaultTabs(), | ||||
|                          var notificationsFilter: String = "[]") { | ||||
|                          var notificationsFilter: String = "[\"follow_request\"]") { | ||||
| 
 | ||||
|     val identifier: String | ||||
|         get() = "$domain:$accountId" | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ import androidx.annotation.NonNull; | |||
| 
 | ||||
| @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, | ||||
|                 TimelineAccountEntity.class,  ConversationEntity.class | ||||
|         }, version = 21) | ||||
|         }, version = 22) | ||||
| public abstract class AppDatabase extends RoomDatabase { | ||||
| 
 | ||||
|     public abstract TootDao tootDao(); | ||||
|  | @ -326,4 +326,11 @@ public abstract class AppDatabase extends RoomDatabase { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
| } | ||||
|     public static final Migration MIGRATION_21_22 = new Migration(21, 22) { | ||||
|         @Override | ||||
|         public void migrate(@NonNull SupportSQLiteDatabase database) { | ||||
|             database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0"); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ class AppModule { | |||
|                         AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, | ||||
|                         AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, | ||||
|                         AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, | ||||
|                         AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21) | ||||
|                         AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22) | ||||
|                 .build() | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ data class Notification( | |||
|         REBLOG("reblog"), | ||||
|         FAVOURITE("favourite"), | ||||
|         FOLLOW("follow"), | ||||
|         FOLLOW_REQUEST("follow_request"), | ||||
|         POLL("poll"); | ||||
| 
 | ||||
|         companion object { | ||||
|  | @ -43,7 +44,7 @@ data class Notification( | |||
|                 } | ||||
|                 return UNKNOWN | ||||
|             } | ||||
|             val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL) | ||||
|             val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL) | ||||
|         } | ||||
| 
 | ||||
|         override fun toString(): String { | ||||
|  |  | |||
|  | @ -65,7 +65,9 @@ import com.keylesspalace.tusky.db.AccountManager; | |||
| import com.keylesspalace.tusky.di.Injectable; | ||||
| import com.keylesspalace.tusky.entity.Notification; | ||||
| import com.keylesspalace.tusky.entity.Poll; | ||||
| import com.keylesspalace.tusky.entity.Relationship; | ||||
| import com.keylesspalace.tusky.entity.Status; | ||||
| import com.keylesspalace.tusky.interfaces.AccountActionListener; | ||||
| import com.keylesspalace.tusky.interfaces.ActionButtonActivity; | ||||
| import com.keylesspalace.tusky.interfaces.ReselectableFragment; | ||||
| import com.keylesspalace.tusky.interfaces.StatusActionListener; | ||||
|  | @ -98,6 +100,7 @@ import javax.inject.Inject; | |||
| 
 | ||||
| import at.connyduck.sparkbutton.helpers.Utils; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import kotlin.Unit; | ||||
| import kotlin.collections.CollectionsKt; | ||||
|  | @ -115,6 +118,7 @@ public class NotificationsFragment extends SFragment implements | |||
|         SwipeRefreshLayout.OnRefreshListener, | ||||
|         StatusActionListener, | ||||
|         NotificationsAdapter.NotificationActionListener, | ||||
|         AccountActionListener, | ||||
|         Injectable, ReselectableFragment { | ||||
|     private static final String TAG = "NotificationF"; // logging tag | ||||
| 
 | ||||
|  | @ -251,7 +255,7 @@ public class NotificationsFragment extends SFragment implements | |||
|         ); | ||||
| 
 | ||||
|         adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), | ||||
|                 dataSource, statusDisplayOptions, this, this); | ||||
|                 dataSource, statusDisplayOptions, this, this, this); | ||||
|         alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); | ||||
|         alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); | ||||
|         recyclerView.setAdapter(adapter); | ||||
|  | @ -765,6 +769,8 @@ public class NotificationsFragment extends SFragment implements | |||
|                 return getString(R.string.notification_boost_name); | ||||
|             case FOLLOW: | ||||
|                 return getString(R.string.notification_follow_name); | ||||
|             case FOLLOW_REQUEST: | ||||
|                 return getString(R.string.notification_follow_request_name); | ||||
|             case POLL: | ||||
|                 return getString(R.string.notification_poll_name); | ||||
|             default: | ||||
|  | @ -817,6 +823,29 @@ public class NotificationsFragment extends SFragment implements | |||
|         super.viewAccount(id); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onMute(boolean mute, String id, int position) { | ||||
|         // No muting from notifications yet | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBlock(boolean block, String id, int position) { | ||||
|         // No blocking from notifications yet | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onRespondToFollowRequest(boolean accept, String id, int position) { | ||||
|         Single<Relationship> request = accept ? | ||||
|                 mastodonApi.authorizeFollowRequestObservable(id) : | ||||
|                 mastodonApi.rejectFollowRequestObservable(id); | ||||
|         request.observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) | ||||
|                 .subscribe( | ||||
|                         (relationship) -> fullyRefreshWithProgressBar(true), | ||||
|                         (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id)) | ||||
|                 ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewStatusForNotificationId(String notificationId) { | ||||
|         for (Either<Placeholder, Notification> either : notifications) { | ||||
|  |  | |||
|  | @ -41,42 +41,23 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O | |||
|         val activeAccount = accountManager.activeAccount | ||||
| 
 | ||||
|         if (activeAccount != null) { | ||||
| 
 | ||||
|             val notificationPref = requirePreference("notificationsEnabled") as SwitchPreferenceCompat | ||||
|             notificationPref.isChecked = activeAccount.notificationsEnabled | ||||
|             notificationPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val mentionedPref = requirePreference("notificationFilterMentions") as SwitchPreferenceCompat | ||||
|             mentionedPref.isChecked = activeAccount.notificationsMentioned | ||||
|             mentionedPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val followedPref = requirePreference("notificationFilterFollows") as SwitchPreferenceCompat | ||||
|             followedPref.isChecked = activeAccount.notificationsFollowed | ||||
|             followedPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val boostedPref = requirePreference("notificationFilterReblogs") as SwitchPreferenceCompat | ||||
|             boostedPref.isChecked = activeAccount.notificationsReblogged | ||||
|             boostedPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val favoritedPref = requirePreference("notificationFilterFavourites") as SwitchPreferenceCompat | ||||
|             favoritedPref.isChecked = activeAccount.notificationsFavorited | ||||
|             favoritedPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val pollsPref = requirePreference("notificationFilterPolls") as SwitchPreferenceCompat | ||||
|             pollsPref.isChecked = activeAccount.notificationsPolls | ||||
|             pollsPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val soundPref = requirePreference("notificationAlertSound") as SwitchPreferenceCompat | ||||
|             soundPref.isChecked = activeAccount.notificationSound | ||||
|             soundPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val vibrationPref = requirePreference("notificationAlertVibrate") as SwitchPreferenceCompat | ||||
|             vibrationPref.isChecked = activeAccount.notificationVibration | ||||
|             vibrationPref.onPreferenceChangeListener = this | ||||
| 
 | ||||
|             val lightPref = requirePreference("notificationAlertLight") as SwitchPreferenceCompat | ||||
|             lightPref.isChecked = activeAccount.notificationLight | ||||
|             lightPref.onPreferenceChangeListener = this | ||||
|             for (pair in mapOf( | ||||
|                     "notificationsEnabled" to activeAccount.notificationsEnabled, | ||||
|                     "notificationFilterMentions" to activeAccount.notificationsMentioned, | ||||
|                     "notificationFilterFollows" to activeAccount.notificationsFollowed, | ||||
|                     "notificationFilterFollowRequests" to activeAccount.notificationsFollowRequested, | ||||
|                     "notificationFilterReblogs" to activeAccount.notificationsReblogged, | ||||
|                     "notificationFilterFavourites" to activeAccount.notificationsFavorited, | ||||
|                     "notificationFilterPolls" to activeAccount.notificationsPolls, | ||||
|                     "notificationAlertSound" to activeAccount.notificationSound, | ||||
|                     "notificationAlertVibrate" to activeAccount.notificationVibration, | ||||
|                     "notificationAlertLight" to activeAccount.notificationLight | ||||
|             )) { | ||||
|                 (requirePreference(pair.key) as SwitchPreferenceCompat).apply { | ||||
|                     isChecked = pair.value | ||||
|                     onPreferenceChangeListener = this@NotificationPreferencesFragment | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -96,6 +77,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O | |||
|                 } | ||||
|                 "notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean | ||||
|                 "notificationFilterFollows" -> activeAccount.notificationsFollowed = newValue as Boolean | ||||
|                 "notificationFilterFollowRequests" -> activeAccount.notificationsFollowRequested = newValue as Boolean | ||||
|                 "notificationFilterReblogs" -> activeAccount.notificationsReblogged = newValue as Boolean | ||||
|                 "notificationFilterFavourites" -> activeAccount.notificationsFavorited = newValue as Boolean | ||||
|                 "notificationFilterPolls" -> activeAccount.notificationsPolls = newValue as Boolean | ||||
|  |  | |||
|  | @ -383,6 +383,16 @@ interface MastodonApi { | |||
|             @Path("id") accountId: String | ||||
|     ): Call<Relationship> | ||||
| 
 | ||||
|     @POST("api/v1/follow_requests/{id}/authorize") | ||||
|     fun authorizeFollowRequestObservable( | ||||
|             @Path("id") accountId: String | ||||
|     ): Single<Relationship> | ||||
| 
 | ||||
|     @POST("api/v1/follow_requests/{id}/reject") | ||||
|     fun rejectFollowRequestObservable( | ||||
|             @Path("id") accountId: String | ||||
|     ): Single<Relationship> | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @POST("api/v1/apps") | ||||
|     fun authenticateApp( | ||||
|  |  | |||
|  | @ -113,6 +113,7 @@ public class NotificationHelper { | |||
|      **/ | ||||
|     public static final String CHANNEL_MENTION = "CHANNEL_MENTION"; | ||||
|     public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW"; | ||||
|     public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST"; | ||||
|     public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; | ||||
|     public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; | ||||
|     public static final String CHANNEL_POLL = "CHANNEL_POLL"; | ||||
|  | @ -348,6 +349,7 @@ public class NotificationHelper { | |||
|             String[] channelIds = new String[]{ | ||||
|                     CHANNEL_MENTION + account.getIdentifier(), | ||||
|                     CHANNEL_FOLLOW + account.getIdentifier(), | ||||
|                     CHANNEL_FOLLOW_REQUEST + account.getIdentifier(), | ||||
|                     CHANNEL_BOOST + account.getIdentifier(), | ||||
|                     CHANNEL_FAVOURITE + account.getIdentifier(), | ||||
|                     CHANNEL_POLL + account.getIdentifier(), | ||||
|  | @ -355,6 +357,7 @@ public class NotificationHelper { | |||
|             int[] channelNames = { | ||||
|                     R.string.notification_mention_name, | ||||
|                     R.string.notification_follow_name, | ||||
|                     R.string.notification_follow_request_name, | ||||
|                     R.string.notification_boost_name, | ||||
|                     R.string.notification_favourite_name, | ||||
|                     R.string.notification_poll_name | ||||
|  | @ -362,12 +365,13 @@ public class NotificationHelper { | |||
|             int[] channelDescriptions = { | ||||
|                     R.string.notification_mention_descriptions, | ||||
|                     R.string.notification_follow_description, | ||||
|                     R.string.notification_follow_request_description, | ||||
|                     R.string.notification_boost_description, | ||||
|                     R.string.notification_favourite_description, | ||||
|                     R.string.notification_poll_description | ||||
|             }; | ||||
| 
 | ||||
|             List<NotificationChannel> channels = new ArrayList<>(5); | ||||
|             List<NotificationChannel> channels = new ArrayList<>(6); | ||||
| 
 | ||||
|             NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); | ||||
| 
 | ||||
|  | @ -508,6 +512,8 @@ public class NotificationHelper { | |||
|                 return account.getNotificationsMentioned(); | ||||
|             case FOLLOW: | ||||
|                 return account.getNotificationsFollowed(); | ||||
|             case FOLLOW_REQUEST: | ||||
|                 return account.getNotificationsFollowRequested(); | ||||
|             case REBLOG: | ||||
|                 return account.getNotificationsReblogged(); | ||||
|             case FAVOURITE: | ||||
|  | @ -525,6 +531,8 @@ public class NotificationHelper { | |||
|                 return CHANNEL_MENTION + account.getIdentifier(); | ||||
|             case FOLLOW: | ||||
|                 return CHANNEL_FOLLOW + account.getIdentifier(); | ||||
|             case FOLLOW_REQUEST: | ||||
|                 return CHANNEL_FOLLOW_REQUEST + account.getIdentifier(); | ||||
|             case REBLOG: | ||||
|                 return CHANNEL_BOOST + account.getIdentifier(); | ||||
|             case FAVOURITE: | ||||
|  | @ -594,6 +602,9 @@ public class NotificationHelper { | |||
|             case FOLLOW: | ||||
|                 return String.format(context.getString(R.string.notification_follow_format), | ||||
|                         accountName); | ||||
|             case FOLLOW_REQUEST: | ||||
|                 return String.format(context.getString(R.string.notification_follow_request_format), | ||||
|                         accountName); | ||||
|             case FAVOURITE: | ||||
|                 return String.format(context.getString(R.string.notification_favourite_format), | ||||
|                         accountName); | ||||
|  | @ -613,6 +624,7 @@ public class NotificationHelper { | |||
|     private static String bodyForType(Notification notification, Context context) { | ||||
|         switch (notification.getType()) { | ||||
|             case FOLLOW: | ||||
|             case FOLLOW_REQUEST: | ||||
|                 return "@" + notification.getAccount().getUsername(); | ||||
|             case MENTION: | ||||
|             case FAVOURITE: | ||||
|  |  | |||
							
								
								
									
										96
									
								
								app/src/main/res/layout/item_follow_request_notification.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/src/main/res/layout/item_follow_request_notification.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:gravity="center_vertical" | ||||
|     android:paddingLeft="16dp" | ||||
|     android:paddingRight="16dp" | ||||
|     android:paddingBottom="10dp"> | ||||
| 
 | ||||
|     <androidx.emoji.widget.EmojiTextView | ||||
|         android:id="@+id/notificationTextView" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:drawableStart="@drawable/ic_person_add_24dp" | ||||
|         android:drawablePadding="10dp" | ||||
|         android:ellipsize="end" | ||||
|         android:gravity="center_vertical" | ||||
|         android:maxLines="1" | ||||
|         android:paddingStart="28dp" | ||||
|         android:textColor="?android:textColorTertiary" | ||||
|         android:textSize="?attr/status_text_medium" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:text="Someone requested to follow you" /> | ||||
| 
 | ||||
|     <ImageView | ||||
|         android:id="@+id/avatar" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="48dp" | ||||
|         android:layout_centerVertical="true" | ||||
|         android:layout_marginTop="10dp" | ||||
|         android:contentDescription="@string/action_view_profile" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@id/notificationTextView" /> | ||||
| 
 | ||||
|     <androidx.emoji.widget.EmojiTextView | ||||
|         android:id="@+id/displayNameTextView" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="14dp" | ||||
|         android:layout_marginTop="6dp" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="1" | ||||
|         android:textColor="?android:textColorPrimary" | ||||
|         android:textSize="?attr/status_text_large" | ||||
|         android:textStyle="normal|bold" | ||||
|         app:layout_constraintStart_toEndOf="@id/avatar" | ||||
|         app:layout_constraintTop_toBottomOf="@id/notificationTextView" | ||||
|         tools:text="Display name" /> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/usernameTextView" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="14dp" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="1" | ||||
|         android:textColor="?android:textColorSecondary" | ||||
|         android:textSize="?attr/status_text_medium" | ||||
|         app:layout_constraintStart_toEndOf="@id/avatar" | ||||
|         app:layout_constraintTop_toBottomOf="@id/displayNameTextView" | ||||
|         tools:text="\@username" /> | ||||
| 
 | ||||
|     <ImageButton | ||||
|         android:id="@+id/acceptButton" | ||||
|         style="@style/TuskyImageButton" | ||||
|         android:layout_width="32dp" | ||||
|         android:layout_height="32dp" | ||||
|         android:layout_centerVertical="true" | ||||
|         android:layout_marginStart="12dp" | ||||
|         android:layout_marginTop="14dp" | ||||
|         android:background="?attr/selectableItemBackgroundBorderless" | ||||
|         android:contentDescription="@string/action_accept" | ||||
|         android:padding="4dp" | ||||
|         app:layout_constraintEnd_toStartOf="@id/rejectButton" | ||||
|         app:layout_constraintTop_toBottomOf="@id/notificationTextView" | ||||
|         app:srcCompat="@drawable/ic_check_24dp" /> | ||||
| 
 | ||||
|     <ImageButton | ||||
|         android:id="@+id/rejectButton" | ||||
|         style="@style/TuskyImageButton" | ||||
|         android:layout_width="32dp" | ||||
|         android:layout_height="32dp" | ||||
|         android:layout_centerVertical="true" | ||||
|         android:layout_marginStart="12dp" | ||||
|         android:layout_marginTop="14dp" | ||||
|         android:background="?attr/selectableItemBackgroundBorderless" | ||||
|         android:contentDescription="@string/action_reject" | ||||
|         android:padding="4dp" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@id/notificationTextView" | ||||
|         app:srcCompat="@drawable/ic_reject_24dp" /> | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | @ -60,6 +60,7 @@ | |||
|     <string name="notification_reblog_format">%s boosted your toot</string> | ||||
|     <string name="notification_favourite_format">%s favorited your toot</string> | ||||
|     <string name="notification_follow_format">%s followed you</string> | ||||
|     <string name="notification_follow_request_format">%s requested to follow you</string> | ||||
| 
 | ||||
|     <string name="report_username_format">Report @%s</string> | ||||
|     <string name="report_comment_hint">Additional comments?</string> | ||||
|  | @ -208,6 +209,7 @@ | |||
|     <string name="pref_title_notification_filters">Notify me when</string> | ||||
|     <string name="pref_title_notification_filter_mentions">mentioned</string> | ||||
|     <string name="pref_title_notification_filter_follows">followed</string> | ||||
|     <string name="pref_title_notification_filter_follow_requests">follow requested</string> | ||||
|     <string name="pref_title_notification_filter_reblogs">my posts are boosted</string> | ||||
|     <string name="pref_title_notification_filter_favourites">my posts are favorited</string> | ||||
|     <string name="pref_title_notification_filter_poll">polls have ended</string> | ||||
|  | @ -262,6 +264,8 @@ | |||
|     <string name="notification_mention_descriptions">Notifications about new mentions</string> | ||||
|     <string name="notification_follow_name">New Followers</string> | ||||
|     <string name="notification_follow_description">Notifications about new followers</string> | ||||
|     <string name="notification_follow_request_name">Follow Requests</string> | ||||
|     <string name="notification_follow_request_description">Notifications about follow requests</string> | ||||
|     <string name="notification_boost_name">Boosts</string> | ||||
|     <string name="notification_boost_description">Notifications when your toots get boosted</string> | ||||
|     <string name="notification_favourite_name">Favorites</string> | ||||
|  |  | |||
|  | @ -27,6 +27,12 @@ | |||
|             android:title="@string/pref_title_notification_filter_follows" | ||||
|             app:iconSpaceReserved="false" /> | ||||
| 
 | ||||
|         <SwitchPreferenceCompat | ||||
|             android:defaultValue="false" | ||||
|             android:key="notificationFilterFollowRequests" | ||||
|             android:title="@string/pref_title_notification_filter_follow_requests" | ||||
|             app:iconSpaceReserved="false" /> | ||||
| 
 | ||||
|         <SwitchPreferenceCompat | ||||
|             android:defaultValue="true" | ||||
|             android:key="notificationFilterReblogs" | ||||
|  |  | |||
|  | @ -68,6 +68,7 @@ class ComposeActivityTest { | |||
|             notificationsEnabled = true, | ||||
|             notificationsMentioned = true, | ||||
|             notificationsFollowed = true, | ||||
|             notificationsFollowRequested = false, | ||||
|             notificationsReblogged = true, | ||||
|             notificationsFavorited = true, | ||||
|             notificationSound = true, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue