mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* migrate conversations and search to paging 3 * delete SearchRepository * remove unneeded executor from search * fix bugs in conversations * update license headers * fix conversations refreshing * fix search refresh indicators * show fullscreen loading while conversations are empty * search bugfixes * error handling * error handling * remove mastodon bug workaround * update ConversationsFragment * fix conversations more menu and deleting conversations * delete unused class * catch exceptions in ConversationsViewModel * fix bug where items are not diffed correctly / cleanup code * fix search progressbar display conditionspull/2208/head
32 changed files with 1616 additions and 1026 deletions
@ -0,0 +1,753 @@ |
|||||||
|
{ |
||||||
|
"formatVersion": 1, |
||||||
|
"database": { |
||||||
|
"version": 27, |
||||||
|
"identityHash": "be914d4eb3f406b6970fef53a925afa1", |
||||||
|
"entities": [ |
||||||
|
{ |
||||||
|
"tableName": "DraftEntity", |
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", |
||||||
|
"fields": [ |
||||||
|
{ |
||||||
|
"fieldPath": "id", |
||||||
|
"columnName": "id", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"notNull": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "accountId", |
||||||
|
"columnName": "accountId", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"notNull": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "inReplyToId", |
||||||
|
"columnName": "inReplyToId", |
||||||
|
"affinity": "TEXT", |
||||||
|
"notNull": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "content", |
||||||
|
"columnName": "content", |
||||||
|
"affinity": "TEXT", |
||||||
|
"notNull": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "contentWarning", |
||||||
|
"columnName": "contentWarning", |
||||||
|
"affinity": "TEXT", |
||||||
|
"notNull": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "sensitive", |
||||||
|
"columnName": "sensitive", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"notNull": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "visibility", |
||||||
|
"columnName": "visibility", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"notNull": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "attachments", |
||||||
|
"columnName": "attachments", |
||||||
|
"affinity": "TEXT", |
||||||
|
"notNull": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "poll", |
||||||
|
"columnName": "poll", |
||||||
|
"affinity": "TEXT", |
||||||
|
"notNull": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "failedToSend", |
||||||
|
"columnName": "failedToSend", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"notNull": true |
||||||
|
} |
||||||
|
], |
||||||
|
"primaryKey": { |
||||||
|
"columnNames": [ |
||||||
|
"id" |
||||||
|
], |
||||||
|
"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, `notificationsSubscriptions` 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": "notificationsSubscriptions", |
||||||
|
"columnName": "notificationsSubscriptions", |
||||||
|
"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, `muted` INTEGER, 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 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"fieldPath": "muted", |
||||||
|
"columnName": "muted", |
||||||
|
"affinity": "INTEGER", |
||||||
|
"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_muted` 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.muted", |
||||||
|
"columnName": "s_muted", |
||||||
|
"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, 'be914d4eb3f406b6970fef53a925afa1')" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
/* Copyright 2021 Tusky Contributors |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||||
|
* Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.conversation |
||||||
|
|
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.paging.LoadState |
||||||
|
import androidx.paging.LoadStateAdapter |
||||||
|
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder |
||||||
|
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding |
||||||
|
|
||||||
|
class ConversationLoadStateAdapter( |
||||||
|
private val retryCallback: () -> Unit |
||||||
|
) : LoadStateAdapter<NetworkStateViewHolder>() { |
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) { |
||||||
|
holder.setUpWithNetworkState(loadState) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateViewHolder( |
||||||
|
parent: ViewGroup, |
||||||
|
loadState: LoadState |
||||||
|
): NetworkStateViewHolder { |
||||||
|
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
||||||
|
return NetworkStateViewHolder(binding, retryCallback) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,98 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (C) 2017 The Android Open Source Project |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation |
|
||||||
|
|
||||||
import androidx.annotation.MainThread |
|
||||||
import androidx.paging.PagedList |
|
||||||
import com.keylesspalace.tusky.entity.Conversation |
|
||||||
import com.keylesspalace.tusky.network.MastodonApi |
|
||||||
import com.keylesspalace.tusky.util.PagingRequestHelper |
|
||||||
import com.keylesspalace.tusky.util.createStatusLiveData |
|
||||||
import retrofit2.Call |
|
||||||
import retrofit2.Callback |
|
||||||
import retrofit2.Response |
|
||||||
import java.util.concurrent.Executor |
|
||||||
|
|
||||||
/** |
|
||||||
* This boundary callback gets notified when user reaches to the edges of the list such that the |
|
||||||
* database cannot provide any more data. |
|
||||||
* <p> |
|
||||||
* The boundary callback might be called multiple times for the same direction so it does its own |
|
||||||
* rate limiting using the PagingRequestHelper class. |
|
||||||
*/ |
|
||||||
class ConversationsBoundaryCallback( |
|
||||||
private val accountId: Long, |
|
||||||
private val mastodonApi: MastodonApi, |
|
||||||
private val handleResponse: (Long, List<Conversation>?) -> Unit, |
|
||||||
private val ioExecutor: Executor, |
|
||||||
private val networkPageSize: Int) |
|
||||||
: PagedList.BoundaryCallback<ConversationEntity>() { |
|
||||||
|
|
||||||
val helper = PagingRequestHelper(ioExecutor) |
|
||||||
val networkState = helper.createStatusLiveData() |
|
||||||
|
|
||||||
/** |
|
||||||
* Database returned 0 items. We should query the backend for more items. |
|
||||||
*/ |
|
||||||
@MainThread |
|
||||||
override fun onZeroItemsLoaded() { |
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { |
|
||||||
mastodonApi.getConversations(null, networkPageSize) |
|
||||||
.enqueue(createWebserviceCallback(it)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* User reached to the end of the list. |
|
||||||
*/ |
|
||||||
@MainThread |
|
||||||
override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) { |
|
||||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { |
|
||||||
mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize) |
|
||||||
.enqueue(createWebserviceCallback(it)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* every time it gets new items, boundary callback simply inserts them into the database and |
|
||||||
* paging library takes care of refreshing the list if necessary. |
|
||||||
*/ |
|
||||||
private fun insertItemsIntoDb( |
|
||||||
response: Response<List<Conversation>>, |
|
||||||
it: PagingRequestHelper.Request.Callback) { |
|
||||||
ioExecutor.execute { |
|
||||||
handleResponse(accountId, response.body()) |
|
||||||
it.recordSuccess() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) { |
|
||||||
// ignored, since we only ever append to what's in the DB |
|
||||||
} |
|
||||||
|
|
||||||
private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback<List<Conversation>> { |
|
||||||
return object : Callback<List<Conversation>> { |
|
||||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) { |
|
||||||
it.recordFailure(t) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) { |
|
||||||
insertItemsIntoDb(response, it) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,51 @@ |
|||||||
|
package com.keylesspalace.tusky.components.conversation |
||||||
|
|
||||||
|
import androidx.paging.ExperimentalPagingApi |
||||||
|
import androidx.paging.LoadType |
||||||
|
import androidx.paging.PagingState |
||||||
|
import androidx.paging.RemoteMediator |
||||||
|
import com.keylesspalace.tusky.db.AppDatabase |
||||||
|
import com.keylesspalace.tusky.network.MastodonApi |
||||||
|
|
||||||
|
@ExperimentalPagingApi |
||||||
|
class ConversationsRemoteMediator( |
||||||
|
private val accountId: Long, |
||||||
|
private val api: MastodonApi, |
||||||
|
private val db: AppDatabase |
||||||
|
) : RemoteMediator<Int, ConversationEntity>() { |
||||||
|
|
||||||
|
override suspend fun load( |
||||||
|
loadType: LoadType, |
||||||
|
state: PagingState<Int, ConversationEntity> |
||||||
|
): MediatorResult { |
||||||
|
|
||||||
|
try { |
||||||
|
val conversationsResult = when (loadType) { |
||||||
|
LoadType.REFRESH -> { |
||||||
|
api.getConversations(limit = state.config.initialLoadSize) |
||||||
|
} |
||||||
|
LoadType.PREPEND -> { |
||||||
|
return MediatorResult.Success(endOfPaginationReached = true) |
||||||
|
} |
||||||
|
LoadType.APPEND -> { |
||||||
|
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id |
||||||
|
api.getConversations(maxId = maxId, limit = state.config.pageSize) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (loadType == LoadType.REFRESH) { |
||||||
|
db.conversationDao().deleteForAccount(accountId) |
||||||
|
} |
||||||
|
db.conversationDao().insert( |
||||||
|
conversationsResult |
||||||
|
.filterNot { it.lastStatus == null } |
||||||
|
.map { it.toEntity(accountId) } |
||||||
|
) |
||||||
|
return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty()) |
||||||
|
} catch (e: Exception) { |
||||||
|
return MediatorResult.Error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH |
||||||
|
} |
||||||
@ -1,126 +0,0 @@ |
|||||||
/* Copyright 2019 Joel Pyska |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
|
||||||
* License, or (at your option) any later version. |
|
||||||
* |
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
||||||
* Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.search.adapter |
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData |
|
||||||
import androidx.paging.PositionalDataSource |
|
||||||
import com.keylesspalace.tusky.components.search.SearchType |
|
||||||
import com.keylesspalace.tusky.entity.SearchResult |
|
||||||
import com.keylesspalace.tusky.network.MastodonApi |
|
||||||
import com.keylesspalace.tusky.util.NetworkState |
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable |
|
||||||
import io.reactivex.rxjava3.kotlin.addTo |
|
||||||
import java.util.concurrent.Executor |
|
||||||
|
|
||||||
class SearchDataSource<T>( |
|
||||||
private val mastodonApi: MastodonApi, |
|
||||||
private val searchType: SearchType, |
|
||||||
private val searchRequest: String, |
|
||||||
private val disposables: CompositeDisposable, |
|
||||||
private val retryExecutor: Executor, |
|
||||||
private val initialItems: List<T>? = null, |
|
||||||
private val parser: (SearchResult?) -> List<T>, |
|
||||||
private val source: SearchDataSourceFactory<T>) : PositionalDataSource<T>() { |
|
||||||
|
|
||||||
val networkState = MutableLiveData<NetworkState>() |
|
||||||
|
|
||||||
private var retry: (() -> Any)? = null |
|
||||||
|
|
||||||
val initialLoad = MutableLiveData<NetworkState>() |
|
||||||
|
|
||||||
fun retry() { |
|
||||||
retry?.let { |
|
||||||
retryExecutor.execute { |
|
||||||
it.invoke() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) { |
|
||||||
if (!initialItems.isNullOrEmpty()) { |
|
||||||
callback.onResult(initialItems.toList(), 0) |
|
||||||
} else { |
|
||||||
networkState.postValue(NetworkState.LOADED) |
|
||||||
retry = null |
|
||||||
initialLoad.postValue(NetworkState.LOADING) |
|
||||||
mastodonApi.searchObservable( |
|
||||||
query = searchRequest, |
|
||||||
type = searchType.apiParameter, |
|
||||||
resolve = true, |
|
||||||
limit = params.requestedLoadSize, |
|
||||||
offset = 0, |
|
||||||
following = false) |
|
||||||
.subscribe( |
|
||||||
{ data -> |
|
||||||
val res = parser(data) |
|
||||||
callback.onResult(res, params.requestedStartPosition) |
|
||||||
initialLoad.postValue(NetworkState.LOADED) |
|
||||||
|
|
||||||
}, |
|
||||||
{ error -> |
|
||||||
retry = { |
|
||||||
loadInitial(params, callback) |
|
||||||
} |
|
||||||
initialLoad.postValue(NetworkState.error(error.message)) |
|
||||||
} |
|
||||||
).addTo(disposables) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) { |
|
||||||
networkState.postValue(NetworkState.LOADING) |
|
||||||
retry = null |
|
||||||
if (source.exhausted) { |
|
||||||
return callback.onResult(emptyList()) |
|
||||||
} |
|
||||||
mastodonApi.searchObservable( |
|
||||||
query = searchRequest, |
|
||||||
type = searchType.apiParameter, |
|
||||||
resolve = true, |
|
||||||
limit = params.loadSize, |
|
||||||
offset = params.startPosition, |
|
||||||
following = false) |
|
||||||
.subscribe( |
|
||||||
{ data -> |
|
||||||
// Working around Mastodon bug where exact match is returned no matter |
|
||||||
// which offset is requested (so if we search for a full username, it's |
|
||||||
// infinite) |
|
||||||
// see https://github.com/tootsuite/mastodon/issues/11365 |
|
||||||
// see https://github.com/tootsuite/mastodon/issues/13083 |
|
||||||
val res = if ((data.accounts.size == 1 && data.accounts[0].username.equals(searchRequest, ignoreCase = true)) |
|
||||||
|| (data.statuses.size == 1 && data.statuses[0].url.equals(searchRequest))) { |
|
||||||
listOf() |
|
||||||
} else { |
|
||||||
parser(data) |
|
||||||
} |
|
||||||
if (res.isEmpty()) { |
|
||||||
source.exhausted = true |
|
||||||
} |
|
||||||
callback.onResult(res) |
|
||||||
networkState.postValue(NetworkState.LOADED) |
|
||||||
}, |
|
||||||
{ error -> |
|
||||||
retry = { |
|
||||||
loadRange(params, callback) |
|
||||||
} |
|
||||||
networkState.postValue(NetworkState.error(error.message)) |
|
||||||
} |
|
||||||
).addTo(disposables) |
|
||||||
|
|
||||||
|
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,83 @@ |
|||||||
|
/* Copyright 2021 Tusky Contributors |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||||
|
* Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.search.adapter |
||||||
|
|
||||||
|
import androidx.paging.PagingSource |
||||||
|
import androidx.paging.PagingState |
||||||
|
import com.keylesspalace.tusky.components.search.SearchType |
||||||
|
import com.keylesspalace.tusky.entity.SearchResult |
||||||
|
import com.keylesspalace.tusky.network.MastodonApi |
||||||
|
import kotlinx.coroutines.rx3.await |
||||||
|
|
||||||
|
class SearchPagingSource<T: Any>( |
||||||
|
private val mastodonApi: MastodonApi, |
||||||
|
private val searchType: SearchType, |
||||||
|
private val searchRequest: String, |
||||||
|
private val initialItems: List<T>?, |
||||||
|
private val parser: (SearchResult) -> List<T>) : PagingSource<Int, T>() { |
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, T>): Int? { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> { |
||||||
|
if (searchRequest.isEmpty()) { |
||||||
|
return LoadResult.Page( |
||||||
|
data = emptyList(), |
||||||
|
prevKey = null, |
||||||
|
nextKey = null |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (params.key == null && !initialItems.isNullOrEmpty()) { |
||||||
|
return LoadResult.Page( |
||||||
|
data = initialItems.toList(), |
||||||
|
prevKey = null, |
||||||
|
nextKey = initialItems.size |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
val currentKey = params.key ?: 0 |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
val data = mastodonApi.searchObservable( |
||||||
|
query = searchRequest, |
||||||
|
type = searchType.apiParameter, |
||||||
|
resolve = true, |
||||||
|
limit = params.loadSize, |
||||||
|
offset = currentKey, |
||||||
|
following = false |
||||||
|
).await() |
||||||
|
|
||||||
|
val res = parser(data) |
||||||
|
|
||||||
|
val nextKey = if (res.isEmpty()) { |
||||||
|
null |
||||||
|
} else { |
||||||
|
currentKey + res.size |
||||||
|
} |
||||||
|
|
||||||
|
return LoadResult.Page( |
||||||
|
data = res, |
||||||
|
prevKey = null, |
||||||
|
nextKey = nextKey |
||||||
|
) |
||||||
|
} catch (e: Exception) { |
||||||
|
return LoadResult.Error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -1,56 +0,0 @@ |
|||||||
/* Copyright 2019 Joel Pyska |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
|
||||||
* License, or (at your option) any later version. |
|
||||||
* |
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
||||||
* Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.search.adapter |
|
||||||
|
|
||||||
import androidx.lifecycle.Transformations |
|
||||||
import androidx.paging.Config |
|
||||||
import androidx.paging.toLiveData |
|
||||||
import com.keylesspalace.tusky.components.search.SearchType |
|
||||||
import com.keylesspalace.tusky.entity.SearchResult |
|
||||||
import com.keylesspalace.tusky.network.MastodonApi |
|
||||||
import com.keylesspalace.tusky.util.Listing |
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable |
|
||||||
import java.util.concurrent.Executors |
|
||||||
|
|
||||||
class SearchRepository<T>(private val mastodonApi: MastodonApi) { |
|
||||||
|
|
||||||
private val executor = Executors.newSingleThreadExecutor() |
|
||||||
|
|
||||||
fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20, |
|
||||||
initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> { |
|
||||||
val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser) |
|
||||||
val livePagedList = sourceFactory.toLiveData( |
|
||||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), |
|
||||||
fetchExecutor = executor |
|
||||||
) |
|
||||||
return Listing( |
|
||||||
pagedList = livePagedList, |
|
||||||
networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { |
|
||||||
it.networkState |
|
||||||
}, |
|
||||||
retry = { |
|
||||||
sourceFactory.sourceLiveData.value?.retry() |
|
||||||
}, |
|
||||||
refresh = { |
|
||||||
sourceFactory.sourceLiveData.value?.invalidate() |
|
||||||
}, |
|
||||||
refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { |
|
||||||
it.initialLoad |
|
||||||
} |
|
||||||
|
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,36 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (C) 2017 The Android Open Source Project |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0 |
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util |
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData |
|
||||||
import androidx.paging.PagedList |
|
||||||
|
|
||||||
/** |
|
||||||
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system |
|
||||||
*/ |
|
||||||
data class Listing<T>( |
|
||||||
// the LiveData of paged lists for the UI to observe |
|
||||||
val pagedList: LiveData<PagedList<T>>, |
|
||||||
// represents the network request status to show to the user |
|
||||||
val networkState: LiveData<NetworkState>, |
|
||||||
// represents the refresh status to show to the user. Separate from networkState, this |
|
||||||
// value is importantly only when refresh is requested. |
|
||||||
val refreshState: LiveData<NetworkState>, |
|
||||||
// refreshes the whole data and fetches it from scratch. |
|
||||||
val refresh: () -> Unit, |
|
||||||
// retries any failed requests. |
|
||||||
val retry: () -> Unit) |
|
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"> |
||||||
|
<item |
||||||
|
android:id="@+id/status_mute_conversation" |
||||||
|
android:title="@string/action_mute_conversation" /> |
||||||
|
<item |
||||||
|
android:id="@+id/status_unmute_conversation" |
||||||
|
android:title="@string/action_unmute_conversation" /> |
||||||
|
<item |
||||||
|
android:id="@+id/conversation_delete" |
||||||
|
android:title="@string/action_delete_conversation" /> |
||||||
|
|
||||||
|
</menu> |
||||||
Loading…
Reference in new issue