move Html parsing to ViewData (#2414)
* move Html parsing to ViewData * refactor reports to use viewdata * cleanup code * refactor conversations * fix getEditableText * rename StatusParsingHelper * fix tests * commit db schema file * add file header * rename helper function to parseAsMastodonHtml * order imports correctly * move mapping off main thread to default dispatcher * fix ktlint
This commit is contained in:
parent
ffbc4b6403
commit
3e849244f9
34 changed files with 1232 additions and 500 deletions
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
Normal file
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
Normal file
|
@ -0,0 +1,809 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 33,
|
||||||
|
"identityHash": "920a0e0c9a600bd236f6bf959b469c18",
|
||||||
|
"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, `notificationsSignUps` 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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` 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": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"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 NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, 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": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` 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.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"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, '920a0e0c9a600bd236f6bf959b469c18')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
import com.keylesspalace.tusky.util.openLink
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
@ -375,12 +375,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.accountFieldData.observe(
|
viewModel.accountFieldData.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
accountFieldAdapter.fields = it
|
accountFieldAdapter.fields = it
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
viewModel.noteSaved.observe(this) {
|
viewModel.noteSaved.observe(this) {
|
||||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||||
}
|
}
|
||||||
|
@ -395,11 +394,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
adapter.refreshContent()
|
adapter.refreshContent()
|
||||||
}
|
}
|
||||||
viewModel.isRefreshing.observe(
|
viewModel.isRefreshing.observe(
|
||||||
this,
|
this
|
||||||
{ isRefreshing ->
|
) { isRefreshing ->
|
||||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +408,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountUsernameTextView.text = usernameFormatted
|
binding.accountUsernameTextView.text = usernameFormatted
|
||||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||||
|
|
||||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||||
|
|
||||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.createClickableText
|
import com.keylesspalace.tusky.util.createClickableText
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
|
|
||||||
class AccountFieldAdapter(
|
class AccountFieldAdapter(
|
||||||
|
@ -65,7 +66,7 @@ class AccountFieldAdapter(
|
||||||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||||
nameTextView.text = emojifiedName
|
nameTextView.text = emojifiedName
|
||||||
|
|
||||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
|
||||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||||
|
|
||||||
if (field.verifiedAt != null) {
|
if (field.verifiedAt != null) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
class ConversationAdapter(
|
class ConversationAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val listener: StatusActionListener
|
private val listener: StatusActionListener
|
||||||
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||||
|
@ -37,17 +37,13 @@ class ConversationAdapter(
|
||||||
holder.setupWithConversation(getItem(position))
|
holder.setupWithConversation(getItem(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun item(position: Int): ConversationEntity? {
|
|
||||||
return getItem(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
||||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
@ -27,7 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -38,7 +37,16 @@ data class ConversationEntity(
|
||||||
val accounts: List<ConversationAccountEntity>,
|
val accounts: List<ConversationAccountEntity>,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||||
|
) {
|
||||||
|
fun toViewData(): ConversationViewData {
|
||||||
|
return ConversationViewData(
|
||||||
|
id = id,
|
||||||
|
accounts = accounts,
|
||||||
|
unread = unread,
|
||||||
|
lastStatus = lastStatus.toViewData()
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ConversationAccountEntity(
|
data class ConversationAccountEntity(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -67,7 +75,7 @@ data class ConversationStatusEntity(
|
||||||
val inReplyToId: String?,
|
val inReplyToId: String?,
|
||||||
val inReplyToAccountId: String?,
|
val inReplyToAccountId: String?,
|
||||||
val account: ConversationAccountEntity,
|
val account: ConversationAccountEntity,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
val createdAt: Date,
|
val createdAt: Date,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
val favouritesCount: Int,
|
val favouritesCount: Int,
|
||||||
|
@ -80,70 +88,14 @@ data class ConversationStatusEntity(
|
||||||
val tags: List<HashTag>?,
|
val tags: List<HashTag>?,
|
||||||
val showingHiddenContent: Boolean,
|
val showingHiddenContent: Boolean,
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsible: Boolean,
|
|
||||||
val collapsed: Boolean,
|
val collapsed: Boolean,
|
||||||
val muted: Boolean,
|
val muted: Boolean,
|
||||||
val poll: Poll?
|
val poll: Poll?
|
||||||
) {
|
) {
|
||||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ConversationStatusEntity
|
fun toViewData(): StatusViewData.Concrete {
|
||||||
|
return StatusViewData.Concrete(
|
||||||
if (id != other.id) return false
|
status = Status(
|
||||||
if (url != other.url) return false
|
|
||||||
if (inReplyToId != other.inReplyToId) return false
|
|
||||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
|
||||||
if (account != other.account) return false
|
|
||||||
if (content.toString() != other.content.toString()) return false
|
|
||||||
if (createdAt != other.createdAt) return false
|
|
||||||
if (emojis != other.emojis) return false
|
|
||||||
if (favouritesCount != other.favouritesCount) return false
|
|
||||||
if (favourited != other.favourited) return false
|
|
||||||
if (sensitive != other.sensitive) return false
|
|
||||||
if (spoilerText != other.spoilerText) return false
|
|
||||||
if (attachments != other.attachments) return false
|
|
||||||
if (mentions != other.mentions) return false
|
|
||||||
if (tags != other.tags) return false
|
|
||||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
|
||||||
if (expanded != other.expanded) return false
|
|
||||||
if (collapsible != other.collapsible) return false
|
|
||||||
if (collapsed != other.collapsed) return false
|
|
||||||
if (muted != other.muted) return false
|
|
||||||
if (poll != other.poll) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + account.hashCode()
|
|
||||||
result = 31 * result + content.toString().hashCode()
|
|
||||||
result = 31 * result + createdAt.hashCode()
|
|
||||||
result = 31 * result + emojis.hashCode()
|
|
||||||
result = 31 * result + favouritesCount
|
|
||||||
result = 31 * result + favourited.hashCode()
|
|
||||||
result = 31 * result + sensitive.hashCode()
|
|
||||||
result = 31 * result + spoilerText.hashCode()
|
|
||||||
result = 31 * result + attachments.hashCode()
|
|
||||||
result = 31 * result + mentions.hashCode()
|
|
||||||
result = 31 * result + tags.hashCode()
|
|
||||||
result = 31 * result + showingHiddenContent.hashCode()
|
|
||||||
result = 31 * result + expanded.hashCode()
|
|
||||||
result = 31 * result + collapsible.hashCode()
|
|
||||||
result = 31 * result + collapsed.hashCode()
|
|
||||||
result = 31 * result + muted.hashCode()
|
|
||||||
result = 31 * result + poll.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toStatus(): Status {
|
|
||||||
return Status(
|
|
||||||
id = id,
|
id = id,
|
||||||
url = url,
|
url = url,
|
||||||
account = account.toAccount(),
|
account = account.toAccount(),
|
||||||
|
@ -169,6 +121,10 @@ data class ConversationStatusEntity(
|
||||||
muted = muted,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null
|
card = null
|
||||||
|
),
|
||||||
|
isExpanded = expanded,
|
||||||
|
isShowingContent = showingHiddenContent,
|
||||||
|
isCollapsed = collapsed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,7 +158,6 @@ fun Status.toEntity() =
|
||||||
tags = tags,
|
tags = tags,
|
||||||
showingHiddenContent = false,
|
showingHiddenContent = false,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
collapsible = shouldTrimStatus(content),
|
|
||||||
collapsed = true,
|
collapsed = true,
|
||||||
muted = muted ?: false,
|
muted = muted ?: false,
|
||||||
poll = poll
|
poll = poll
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* Copyright 2022 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 com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
|
data class ConversationViewData(
|
||||||
|
val id: String,
|
||||||
|
val accounts: List<ConversationAccountEntity>,
|
||||||
|
val unread: Boolean,
|
||||||
|
val lastStatus: StatusViewData.Concrete
|
||||||
|
) {
|
||||||
|
fun toEntity(
|
||||||
|
accountId: Long,
|
||||||
|
favourited: Boolean = lastStatus.status.favourited,
|
||||||
|
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||||
|
muted: Boolean = lastStatus.status.muted ?: false,
|
||||||
|
poll: Poll? = lastStatus.status.poll,
|
||||||
|
expanded: Boolean = lastStatus.isExpanded,
|
||||||
|
collapsed: Boolean = lastStatus.isCollapsed,
|
||||||
|
showingHiddenContent: Boolean = lastStatus.isShowingContent
|
||||||
|
): ConversationEntity {
|
||||||
|
return ConversationEntity(
|
||||||
|
accountId = accountId,
|
||||||
|
id = id,
|
||||||
|
accounts = accounts,
|
||||||
|
unread = unread,
|
||||||
|
lastStatus = lastStatus.toConversationStatusEntity(
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
muted = muted,
|
||||||
|
poll = poll,
|
||||||
|
expanded = expanded,
|
||||||
|
collapsed = collapsed,
|
||||||
|
showingHiddenContent = showingHiddenContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
|
favourited: Boolean = status.favourited,
|
||||||
|
bookmarked: Boolean = status.bookmarked,
|
||||||
|
muted: Boolean = status.muted ?: false,
|
||||||
|
poll: Poll? = status.poll,
|
||||||
|
expanded: Boolean = isExpanded,
|
||||||
|
collapsed: Boolean = isCollapsed,
|
||||||
|
showingHiddenContent: Boolean = isShowingContent
|
||||||
|
): ConversationStatusEntity {
|
||||||
|
return ConversationStatusEntity(
|
||||||
|
id = id,
|
||||||
|
url = status.url,
|
||||||
|
inReplyToId = status.inReplyToId,
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
|
account = status.account.toEntity(),
|
||||||
|
content = status.content,
|
||||||
|
createdAt = status.createdAt,
|
||||||
|
emojis = status.emojis,
|
||||||
|
favouritesCount = status.favouritesCount,
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
spoilerText = status.spoilerText,
|
||||||
|
attachments = status.attachments,
|
||||||
|
mentions = status.mentions,
|
||||||
|
tags = status.tags,
|
||||||
|
showingHiddenContent = showingHiddenContent,
|
||||||
|
expanded = expanded,
|
||||||
|
collapsed = collapsed,
|
||||||
|
muted = muted,
|
||||||
|
poll = poll
|
||||||
|
)
|
||||||
|
}
|
|
@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithConversation(ConversationEntity conversation) {
|
void setupWithConversation(ConversationViewData conversation) {
|
||||||
ConversationStatusEntity status = conversation.getLastStatus();
|
StatusViewData.Concrete statusViewData = conversation.getLastStatus();
|
||||||
ConversationAccountEntity account = status.getAccount();
|
Status status = statusViewData.getStatus();
|
||||||
|
TimelineAccount account = status.getAccount();
|
||||||
|
|
||||||
setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener);
|
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
|
||||||
|
|
||||||
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
||||||
setUsername(account.getUsername());
|
setUsername(account.getUsername());
|
||||||
|
@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
List<Attachment> attachments = status.getAttachments();
|
List<Attachment> attachments = status.getAttachments();
|
||||||
boolean sensitive = status.getSensitive();
|
boolean sensitive = status.getSensitive();
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
|
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||||
statusDisplayOptions.useBlurhash());
|
statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
if (attachments.size() == 0) {
|
if (attachments.size() == 0) {
|
||||||
|
@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
mediaLabel.setVisibility(View.GONE);
|
mediaLabel.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent());
|
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
||||||
// Hide all unused views.
|
// Hide all unused views.
|
||||||
mediaPreviews[0].setVisibility(View.GONE);
|
mediaPreviews[0].setVisibility(View.GONE);
|
||||||
mediaPreviews[1].setVisibility(View.GONE);
|
mediaPreviews[1].setVisibility(View.GONE);
|
||||||
|
@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupButtons(listener, account.getId(), status.getContent().toString(),
|
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
||||||
statusDisplayOptions);
|
statusDisplayOptions);
|
||||||
|
|
||||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
|
||||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||||
|
|
||||||
|
|
|
@ -153,24 +153,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.favourite(favourite, conversation)
|
viewModel.favourite(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.bookmark(favourite, conversation)
|
viewModel.bookmark(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMore(view: View, position: Int) {
|
override fun onMore(view: View, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
|
|
||||||
val popup = PopupMenu(requireContext(), view)
|
val popup = PopupMenu(requireContext(), view)
|
||||||
popup.inflate(R.menu.conversation_more)
|
popup.inflate(R.menu.conversation_more)
|
||||||
|
|
||||||
if (conversation.lastStatus.muted) {
|
if (conversation.lastStatus.status.muted == true) {
|
||||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||||
} else {
|
} else {
|
||||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||||
|
@ -189,14 +189,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewThread(position: Int) {
|
override fun onViewThread(position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,13 +205,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.expandHiddenStatus(expanded, conversation)
|
viewModel.expandHiddenStatus(expanded, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.showContent(isShowing, conversation)
|
viewModel.showContent(isShowing, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,7 +221,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.collapseLongStatus(isCollapsed, conversation)
|
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,12 +241,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
reply(conversation.lastStatus.toStatus())
|
reply(conversation.lastStatus.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteConversation(conversation: ConversationEntity) {
|
private fun deleteConversation(conversation: ConversationViewData) {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -268,7 +268,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.voteInPoll(choices, conversation)
|
viewModel.voteInPoll(choices, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,18 @@
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.ExperimentalPagingApi
|
import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val api: MastodonApi
|
private val api: MastodonApi
|
||||||
) : RxAwareViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val conversationFlow = Pager(
|
val conversationFlow = Pager(
|
||||||
|
@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor(
|
||||||
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
|
.map { pagingData ->
|
||||||
|
pagingData.map { conversation -> conversation.toViewData() }
|
||||||
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun favourite(favourite: Boolean, conversation: ConversationEntity) {
|
fun favourite(favourite: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
favourited = favourite
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to favourite status", e)
|
Log.w(TAG, "failed to favourite status", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmark(bookmark: Boolean, conversation: ConversationEntity) {
|
fun bookmark(bookmark: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
bookmarked = bookmark
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to bookmark status", e)
|
Log.w(TAG, "failed to bookmark status", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun voteInPoll(choices: List<Int>, conversation: ConversationEntity) {
|
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
|
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await()
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
poll = poll
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to vote in poll", e)
|
Log.w(TAG, "failed to vote in poll", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
|
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
expanded = expanded
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
|
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
collapsed = collapsed
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showContent(showing: Boolean, conversation: ConversationEntity) {
|
fun showContent(showing: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
showingHiddenContent = showing
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(conversation: ConversationEntity) {
|
fun remove(conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
api.deleteConversation(conversationId = conversation.id)
|
api.deleteConversation(conversationId = conversation.id)
|
||||||
|
|
||||||
database.conversationDao().delete(conversation)
|
database.conversationDao().delete(
|
||||||
|
id = conversation.id,
|
||||||
|
accountId = accountManager.activeAccount!!.id
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to delete conversation", e)
|
Log.w(TAG, "failed to delete conversation", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun muteConversation(conversation: ConversationEntity) {
|
fun muteConversation(conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val newStatus = timelineCases.muteConversation(
|
timelineCases.muteConversation(
|
||||||
conversation.lastStatus.id,
|
conversation.lastStatus.id,
|
||||||
!conversation.lastStatus.muted
|
!(conversation.lastStatus.status.muted ?: false)
|
||||||
).await()
|
).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = newStatus.toEntity()
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
muted = !(conversation.lastStatus.status.muted ?: false)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
@ -151,7 +166,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
private suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
||||||
database.conversationDao().insert(conversation)
|
database.conversationDao().insert(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||||
|
@ -34,11 +35,13 @@ import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -73,6 +76,11 @@ class ReportViewModel @Inject constructor(
|
||||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
||||||
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
||||||
).flow
|
).flow
|
||||||
|
}
|
||||||
|
.map { pagingData ->
|
||||||
|
/* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete
|
||||||
|
instead of StatusViewState */
|
||||||
|
pagingData.map { status -> status.toViewData(false, false, false) }
|
||||||
}
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ relationship ->
|
{ relationship ->
|
||||||
val muting = relationship?.muting == true
|
val muting = relationship.muting
|
||||||
muteStateMutable.value = Success(muting)
|
muteStateMutable.value = Success(muting)
|
||||||
if (muting) {
|
if (muting) {
|
||||||
eventHub.dispatch(MuteEvent(accountId))
|
eventHub.dispatch(MuteEvent(accountId))
|
||||||
|
@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ relationship ->
|
{ relationship ->
|
||||||
val blocking = relationship?.blocking == true
|
val blocking = relationship.blocking
|
||||||
blockStateMutable.value = Success(blocking)
|
blockStateMutable.value = Success(blocking)
|
||||||
if (blocking) {
|
if (blocking) {
|
||||||
eventHub.dispatch(BlockEvent(accountId))
|
eventHub.dispatch(BlockEvent(accountId))
|
||||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.toViewData
|
import com.keylesspalace.tusky.viewdata.toViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -45,20 +46,21 @@ class StatusViewHolder(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val viewState: StatusViewState,
|
private val viewState: StatusViewState,
|
||||||
private val adapterHandler: AdapterHandler,
|
private val adapterHandler: AdapterHandler,
|
||||||
private val getStatusForPosition: (Int) -> Status?
|
private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||||
private val statusViewHelper = StatusViewHelper(itemView)
|
private val statusViewHelper = StatusViewHelper(itemView)
|
||||||
|
|
||||||
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
||||||
override fun onViewMedia(v: View?, idx: Int) {
|
override fun onViewMedia(v: View?, idx: Int) {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
adapterHandler.showMedia(v, status, idx)
|
adapterHandler.showMedia(v, viewdata.status, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean) {
|
override fun onContentHiddenChange(isShowing: Boolean) {
|
||||||
status()?.id?.let { id ->
|
viewdata()?.id?.let { id ->
|
||||||
viewState.setMediaShow(id, isShowing)
|
viewState.setMediaShow(id, isShowing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,57 +68,57 @@ class StatusViewHolder(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
adapterHandler.setStatusChecked(status, isChecked)
|
adapterHandler.setStatusChecked(viewdata.status, isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.statusMediaPreviewContainer.clipToOutline = true
|
binding.statusMediaPreviewContainer.clipToOutline = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(status: Status) {
|
fun bind(viewData: StatusViewData.Concrete) {
|
||||||
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
|
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id)
|
||||||
|
|
||||||
updateTextView()
|
updateTextView()
|
||||||
|
|
||||||
val sensitive = status.sensitive
|
val sensitive = viewData.status.sensitive
|
||||||
|
|
||||||
statusViewHelper.setMediasPreview(
|
statusViewHelper.setMediasPreview(
|
||||||
statusDisplayOptions, status.attachments,
|
statusDisplayOptions, viewData.status.attachments,
|
||||||
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
|
sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive),
|
||||||
mediaViewHeight
|
mediaViewHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
|
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
|
||||||
setCreatedAt(status.createdAt)
|
setCreatedAt(viewData.status.createdAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTextView() {
|
private fun updateTextView() {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
setupCollapsedState(
|
setupCollapsedState(
|
||||||
shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
|
shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true),
|
||||||
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
|
viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText
|
||||||
)
|
)
|
||||||
|
|
||||||
if (status.spoilerText.isBlank()) {
|
if (viewdata.spoilerText.isBlank()) {
|
||||||
setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
binding.statusContentWarningButton.hide()
|
binding.statusContentWarningButton.hide()
|
||||||
binding.statusContentWarningDescription.hide()
|
binding.statusContentWarningDescription.hide()
|
||||||
} else {
|
} else {
|
||||||
val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
||||||
binding.statusContentWarningDescription.text = emojiSpoiler
|
binding.statusContentWarningDescription.text = emojiSpoiler
|
||||||
binding.statusContentWarningDescription.show()
|
binding.statusContentWarningDescription.show()
|
||||||
binding.statusContentWarningButton.show()
|
binding.statusContentWarningButton.show()
|
||||||
setContentWarningButtonText(viewState.isContentShow(status.id, true))
|
setContentWarningButtonText(viewState.isContentShow(viewdata.id, true))
|
||||||
binding.statusContentWarningButton.setOnClickListener {
|
binding.statusContentWarningButton.setOnClickListener {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
val contentShown = viewState.isContentShow(status.id, true)
|
val contentShown = viewState.isContentShow(viewdata.id, true)
|
||||||
binding.statusContentWarningDescription.invalidate()
|
binding.statusContentWarningDescription.invalidate()
|
||||||
viewState.setContentShow(status.id, !contentShown)
|
viewState.setContentShow(viewdata.id, !contentShown)
|
||||||
setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
setContentWarningButtonText(!contentShown)
|
setContentWarningButtonText(!contentShown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,8 +171,8 @@ class StatusViewHolder(
|
||||||
/* input filter for TextViews have to be set before text */
|
/* input filter for TextViews have to be set before text */
|
||||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||||
binding.buttonToggleContent.setOnClickListener {
|
binding.buttonToggleContent.setOnClickListener {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
viewState.setCollapsed(status.id, !collapsed)
|
viewState.setCollapsed(viewdata.id, !collapsed)
|
||||||
updateTextView()
|
updateTextView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,5 +191,5 @@ class StatusViewHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun status() = getStatusForPosition(bindingAdapterPosition)
|
private fun viewdata() = getStatusForPosition(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||||
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
class StatusesAdapter(
|
class StatusesAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val statusViewState: StatusViewState,
|
private val statusViewState: StatusViewState,
|
||||||
private val adapterHandler: AdapterHandler
|
private val adapterHandler: AdapterHandler
|
||||||
) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
|
) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||||
|
|
||||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int ->
|
||||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,11 +50,11 @@ class StatusesAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
|
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||||
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
|
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
|
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||||
|
@ -29,8 +26,6 @@ import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -119,7 +114,7 @@ fun Status.toEntity(
|
||||||
authorServerId = actionableStatus.account.id,
|
authorServerId = actionableStatus.account.id,
|
||||||
inReplyToId = actionableStatus.inReplyToId,
|
inReplyToId = actionableStatus.inReplyToId,
|
||||||
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
||||||
content = actionableStatus.content.toHtml(),
|
content = actionableStatus.content,
|
||||||
createdAt = actionableStatus.createdAt.time,
|
createdAt = actionableStatus.createdAt.time,
|
||||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
emojis = actionableStatus.emojis.let(gson::toJson),
|
||||||
reblogsCount = actionableStatus.reblogsCount,
|
reblogsCount = actionableStatus.reblogsCount,
|
||||||
|
@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
content = status.content.orEmpty(),
|
||||||
?: SpannedString(""),
|
|
||||||
createdAt = Date(status.createdAt),
|
createdAt = Date(status.createdAt),
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
reblogsCount = status.reblogsCount,
|
reblogsCount = status.reblogsCount,
|
||||||
|
@ -195,7 +189,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = reblog,
|
reblog = reblog,
|
||||||
content = SpannedString(""),
|
content = "",
|
||||||
createdAt = Date(status.createdAt), // lie but whatever?
|
createdAt = Date(status.createdAt), // lie but whatever?
|
||||||
emojis = listOf(),
|
emojis = listOf(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
|
@ -223,8 +217,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
content = status.content.orEmpty(),
|
||||||
?: SpannedString(""),
|
|
||||||
createdAt = Date(status.createdAt),
|
createdAt = Date(status.createdAt),
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
reblogsCount = status.reblogsCount,
|
reblogsCount = status.reblogsCount,
|
||||||
|
@ -249,7 +242,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
status = status,
|
status = status,
|
||||||
isExpanded = this.status.expanded,
|
isExpanded = this.status.expanded,
|
||||||
isShowingContent = this.status.contentShowing,
|
isShowingContent = this.status.contentShowing,
|
||||||
isCollapsible = shouldTrimStatus(status.content),
|
|
||||||
isCollapsed = this.status.contentCollapsed
|
isCollapsed = this.status.contentCollapsed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,10 @@ import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -79,15 +82,13 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
).flow
|
).flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map { timelineStatus ->
|
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
||||||
timelineStatus.toViewData(gson)
|
timelineStatus.toViewData(gson)
|
||||||
}
|
}.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||||
}
|
|
||||||
.map { pagingData ->
|
|
||||||
pagingData.filter { statusViewData ->
|
|
||||||
!shouldFilterStatus(statusViewData)
|
!shouldFilterStatus(statusViewData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan
|
||||||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -79,10 +82,11 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
|
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
|
||||||
).flow
|
).flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.filter { statusViewData ->
|
pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||||
!shouldFilterStatus(statusViewData)
|
!shouldFilterStatus(statusViewData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 32)
|
}, version = 33)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -490,4 +490,41 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1");
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_32_33 = new Migration(32, 33) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
|
||||||
|
// ConversationEntity lost the s_collapsible column
|
||||||
|
// since SQLite does not support removing columns and it is just a cache table, we recreate the whole table.
|
||||||
|
database.execSQL("DROP TABLE `ConversationEntity`");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" +
|
||||||
|
"`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_tags` TEXT," +
|
||||||
|
"`s_showingHiddenContent` INTEGER NOT NULL," +
|
||||||
|
"`s_expanded` INTEGER NOT NULL," +
|
||||||
|
"`s_collapsed` INTEGER NOT NULL," +
|
||||||
|
"`s_muted` INTEGER NOT NULL," +
|
||||||
|
"`s_poll` TEXT," +
|
||||||
|
"PRIMARY KEY(`id`, `accountId`))");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
@ -31,8 +30,8 @@ interface ConversationsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(conversation: ConversationEntity): Long
|
suspend fun insert(conversation: ConversationEntity): Long
|
||||||
|
|
||||||
@Delete
|
@Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId")
|
||||||
suspend fun delete(conversation: ConversationEntity): Int
|
suspend fun delete(id: String, accountId: Long): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
||||||
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
||||||
|
|
|
@ -15,9 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.db
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import androidx.room.ProvidedTypeConverter
|
import androidx.room.ProvidedTypeConverter
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -31,10 +28,8 @@ import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -140,22 +135,6 @@ class Converters @Inject constructor (
|
||||||
return Date(date)
|
return Date(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun spannedToString(spanned: Spanned?): String? {
|
|
||||||
if (spanned == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return spanned.toHtml()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun stringToSpanned(spannedString: String?): Spanned? {
|
|
||||||
if (spannedString == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return spannedString.parseAsHtml().trimTrailingWhitespace()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun pollToJson(poll: Poll?): String? {
|
fun pollToJson(poll: Poll?): String? {
|
||||||
return gson.toJson(poll)
|
return gson.toJson(poll)
|
||||||
|
|
|
@ -63,6 +63,7 @@ class AppModule {
|
||||||
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
||||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||||
|
AppDatabase.MIGRATION_32_33
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,10 @@ package com.keylesspalace.tusky.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.Spanned
|
|
||||||
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
|
||||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
@ -52,11 +49,7 @@ class NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson(): Gson {
|
fun providesGson() = Gson()
|
||||||
return GsonBuilder()
|
|
||||||
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@ data class Account(
|
||||||
@SerializedName("username") val localUsername: String,
|
@SerializedName("username") val localUsername: String,
|
||||||
@SerializedName("acct") val username: String,
|
@SerializedName("acct") val username: String,
|
||||||
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||||
val note: Spanned,
|
val note: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val header: String,
|
val header: String,
|
||||||
|
@ -46,56 +45,6 @@ data class Account(
|
||||||
} else displayName
|
} else displayName
|
||||||
|
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
fun isRemote(): Boolean = this.username != this.localUsername
|
||||||
|
|
||||||
/**
|
|
||||||
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
|
||||||
*/
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
other as Account
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
|
||||||
if (localUsername != other.localUsername) return false
|
|
||||||
if (username != other.username) return false
|
|
||||||
if (displayName != other.displayName) return false
|
|
||||||
if (note.toString() != other.note.toString()) return false
|
|
||||||
if (url != other.url) return false
|
|
||||||
if (avatar != other.avatar) return false
|
|
||||||
if (header != other.header) return false
|
|
||||||
if (locked != other.locked) return false
|
|
||||||
if (followersCount != other.followersCount) return false
|
|
||||||
if (followingCount != other.followingCount) return false
|
|
||||||
if (statusesCount != other.statusesCount) return false
|
|
||||||
if (source != other.source) return false
|
|
||||||
if (bot != other.bot) return false
|
|
||||||
if (emojis != other.emojis) return false
|
|
||||||
if (fields != other.fields) return false
|
|
||||||
if (moved != other.moved) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + localUsername.hashCode()
|
|
||||||
result = 31 * result + username.hashCode()
|
|
||||||
result = 31 * result + (displayName?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + note.toString().hashCode()
|
|
||||||
result = 31 * result + url.hashCode()
|
|
||||||
result = 31 * result + avatar.hashCode()
|
|
||||||
result = 31 * result + header.hashCode()
|
|
||||||
result = 31 * result + locked.hashCode()
|
|
||||||
result = 31 * result + followersCount
|
|
||||||
result = 31 * result + followingCount
|
|
||||||
result = 31 * result + statusesCount
|
|
||||||
result = 31 * result + (source?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + bot.hashCode()
|
|
||||||
result = 31 * result + (emojis?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (fields?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (moved?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
|
@ -107,7 +56,7 @@ data class AccountSource(
|
||||||
|
|
||||||
data class Field(
|
data class Field(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: Spanned,
|
val value: String,
|
||||||
@SerializedName("verified_at") val verifiedAt: Date?
|
@SerializedName("verified_at") val verifiedAt: Date?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
data class Announcement(
|
data class Announcement(
|
||||||
val id: String,
|
val id: String,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
@SerializedName("starts_at") val startsAt: Date?,
|
@SerializedName("starts_at") val startsAt: Date?,
|
||||||
@SerializedName("ends_at") val endsAt: Date?,
|
@SerializedName("ends_at") val endsAt: Date?,
|
||||||
@SerializedName("all_day") val allDay: Boolean,
|
@SerializedName("all_day") val allDay: Boolean,
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class Card(
|
data class Card(
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: Spanned,
|
val title: String,
|
||||||
val description: Spanned,
|
val description: String,
|
||||||
@SerializedName("author_name") val authorName: String,
|
@SerializedName("author_name") val authorName: String,
|
||||||
val image: String,
|
val image: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
@ -31,9 +30,7 @@ data class Card(
|
||||||
val embed_url: String?
|
val embed_url: String?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = url.hashCode()
|
||||||
return url.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Card) {
|
if (other !is Card) {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ data class Status(
|
||||||
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
||||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||||
val reblog: Status?,
|
val reblog: Status?,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@SerializedName("created_at") val createdAt: Date,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||||
|
@ -134,8 +134,9 @@ data class Status(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEditableText(): String {
|
private fun getEditableText(): String {
|
||||||
val builder = SpannableStringBuilder(content)
|
val contentSpanned = content.parseAsMastodonHtml()
|
||||||
for (span in content.getSpans(0, content.length, URLSpan::class.java)) {
|
val builder = SpannableStringBuilder(content.parseAsMastodonHtml())
|
||||||
|
for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) {
|
||||||
val url = span.url
|
val url = span.url
|
||||||
for ((_, url1, username) in mentions) {
|
for ((_, url1, username) in mentions) {
|
||||||
if (url == url1) {
|
if (url == url1) {
|
||||||
|
@ -149,71 +150,6 @@ data class Status(
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
|
||||||
*/
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
other as Status
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
|
||||||
if (url != other.url) return false
|
|
||||||
if (account != other.account) return false
|
|
||||||
if (inReplyToId != other.inReplyToId) return false
|
|
||||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
|
||||||
if (reblog != other.reblog) return false
|
|
||||||
if (content.toString() != other.content.toString()) return false
|
|
||||||
if (createdAt != other.createdAt) return false
|
|
||||||
if (emojis != other.emojis) return false
|
|
||||||
if (reblogsCount != other.reblogsCount) return false
|
|
||||||
if (favouritesCount != other.favouritesCount) return false
|
|
||||||
if (reblogged != other.reblogged) return false
|
|
||||||
if (favourited != other.favourited) return false
|
|
||||||
if (bookmarked != other.bookmarked) return false
|
|
||||||
if (sensitive != other.sensitive) return false
|
|
||||||
if (spoilerText != other.spoilerText) return false
|
|
||||||
if (visibility != other.visibility) return false
|
|
||||||
if (attachments != other.attachments) return false
|
|
||||||
if (mentions != other.mentions) return false
|
|
||||||
if (tags != other.tags) return false
|
|
||||||
if (application != other.application) return false
|
|
||||||
if (pinned != other.pinned) return false
|
|
||||||
if (muted != other.muted) return false
|
|
||||||
if (poll != other.poll) return false
|
|
||||||
if (card != other.card) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + account.hashCode()
|
|
||||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (reblog?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + content.toString().hashCode()
|
|
||||||
result = 31 * result + createdAt.hashCode()
|
|
||||||
result = 31 * result + emojis.hashCode()
|
|
||||||
result = 31 * result + reblogsCount
|
|
||||||
result = 31 * result + favouritesCount
|
|
||||||
result = 31 * result + reblogged.hashCode()
|
|
||||||
result = 31 * result + favourited.hashCode()
|
|
||||||
result = 31 * result + bookmarked.hashCode()
|
|
||||||
result = 31 * result + sensitive.hashCode()
|
|
||||||
result = 31 * result + spoilerText.hashCode()
|
|
||||||
result = 31 * result + visibility.hashCode()
|
|
||||||
result = 31 * result + attachments.hashCode()
|
|
||||||
result = 31 * result + mentions.hashCode()
|
|
||||||
result = 31 * result + (tags?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (application?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (pinned?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (muted?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (poll?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (card?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Mention(
|
data class Mention(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
/* Copyright 2020 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.json
|
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class SpannedTypeAdapter : JsonDeserializer<Spanned>, JsonSerializer<Spanned?> {
|
|
||||||
@Throws(JsonParseException::class)
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned {
|
|
||||||
return json.asString
|
|
||||||
/* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api.
|
|
||||||
* We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior.
|
|
||||||
*/
|
|
||||||
?.replace("<br> ", "<br> ")
|
|
||||||
?.replace("<br /> ", "<br /> ")
|
|
||||||
?.replace("<br/> ", "<br/> ")
|
|
||||||
?.replace(" ", " ")
|
|
||||||
?.parseAsHtml()
|
|
||||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
|
||||||
* most status contents do, so it should be trimmed. */
|
|
||||||
?.trimTrailingWhitespace()
|
|
||||||
?: SpannedString("")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
|
||||||
return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* Copyright 2022 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.util
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse a String containing html from the Mastodon api to Spanned
|
||||||
|
*/
|
||||||
|
fun String.parseAsMastodonHtml(): Spanned {
|
||||||
|
return this.replace("<br> ", "<br> ")
|
||||||
|
.replace("<br /> ", "<br /> ")
|
||||||
|
.replace("<br/> ", "<br/> ")
|
||||||
|
.replace(" ", " ")
|
||||||
|
.parseAsHtml()
|
||||||
|
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||||
|
* most status contents do, so it should be trimmed. */
|
||||||
|
.trimTrailingWhitespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
||||||
|
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
||||||
|
var replacing = false
|
||||||
|
var builder: SpannableStringBuilder? = null
|
||||||
|
val length = content.length
|
||||||
|
for (index in 0 until length) {
|
||||||
|
val character = content[index]
|
||||||
|
|
||||||
|
// If there are more than one or two, switch to a map
|
||||||
|
if (character == SOFT_HYPHEN) {
|
||||||
|
if (!replacing) {
|
||||||
|
replacing = true
|
||||||
|
builder = SpannableStringBuilder(content, 0, index)
|
||||||
|
}
|
||||||
|
builder!!.append(ASCII_HYPHEN)
|
||||||
|
} else if (replacing) {
|
||||||
|
builder!!.append(character)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (replacing) builder else content
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SOFT_HYPHEN = '\u00ad'
|
||||||
|
private const val ASCII_HYPHEN = '-'
|
|
@ -27,12 +27,9 @@ fun Status.toViewData(
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
isCollapsed: Boolean
|
isCollapsed: Boolean
|
||||||
): StatusViewData.Concrete {
|
): StatusViewData.Concrete {
|
||||||
val visibleStatus = this.reblog ?: this
|
|
||||||
|
|
||||||
return StatusViewData.Concrete(
|
return StatusViewData.Concrete(
|
||||||
status = this,
|
status = this,
|
||||||
isShowingContent = isShowingContent,
|
isShowingContent = isShowingContent,
|
||||||
isCollapsible = shouldTrimStatus(visibleStatus.content),
|
|
||||||
isCollapsed = isCollapsed,
|
isCollapsed = isCollapsed,
|
||||||
isExpanded = isExpanded,
|
isExpanded = isExpanded,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
package com.keylesspalace.tusky.viewdata
|
package com.keylesspalace.tusky.viewdata
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
|
import com.keylesspalace.tusky.util.replaceCrashingCharacters
|
||||||
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 11/07/2017.
|
* Created by charlag on 11/07/2017.
|
||||||
|
@ -32,13 +34,6 @@ sealed class StatusViewData {
|
||||||
val status: Status,
|
val status: Status,
|
||||||
val isExpanded: Boolean,
|
val isExpanded: Boolean,
|
||||||
val isShowingContent: Boolean,
|
val isShowingContent: Boolean,
|
||||||
/**
|
|
||||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
|
||||||
* all content regardless.
|
|
||||||
*
|
|
||||||
* @return Whether the post is collapsible or never collapsed.
|
|
||||||
*/
|
|
||||||
val isCollapsible: Boolean,
|
|
||||||
/**
|
/**
|
||||||
* Specifies whether the content of this post is currently limited in visibility to the first
|
* Specifies whether the content of this post is currently limited in visibility to the first
|
||||||
* 500 characters or not.
|
* 500 characters or not.
|
||||||
|
@ -51,6 +46,14 @@ sealed class StatusViewData {
|
||||||
override val id: String
|
override val id: String
|
||||||
get() = status.id
|
get() = status.id
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||||
|
* all content regardless.
|
||||||
|
*
|
||||||
|
* @return Whether the post is collapsible or never collapsed.
|
||||||
|
*/
|
||||||
|
val isCollapsible: Boolean
|
||||||
|
|
||||||
val content: Spanned
|
val content: Spanned
|
||||||
val spoilerText: String
|
val spoilerText: String
|
||||||
val username: String
|
val username: String
|
||||||
|
@ -74,45 +77,17 @@ sealed class StatusViewData {
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT == 23) {
|
if (Build.VERSION.SDK_INT == 23) {
|
||||||
// https://github.com/tuskyapp/Tusky/issues/563
|
// https://github.com/tuskyapp/Tusky/issues/563
|
||||||
this.content = replaceCrashingCharacters(status.actionableStatus.content)
|
this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml())
|
||||||
this.spoilerText =
|
this.spoilerText =
|
||||||
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
||||||
this.username =
|
this.username =
|
||||||
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
||||||
} else {
|
} else {
|
||||||
this.content = status.actionableStatus.content
|
this.content = status.actionableStatus.content.parseAsMastodonHtml()
|
||||||
this.spoilerText = status.actionableStatus.spoilerText
|
this.spoilerText = status.actionableStatus.spoilerText
|
||||||
this.username = status.actionableStatus.account.username
|
this.username = status.actionableStatus.account.username
|
||||||
}
|
}
|
||||||
}
|
this.isCollapsible = shouldTrimStatus(this.content)
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SOFT_HYPHEN = '\u00ad'
|
|
||||||
private const val ASCII_HYPHEN = '-'
|
|
||||||
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
|
||||||
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
|
||||||
var replacing = false
|
|
||||||
var builder: SpannableStringBuilder? = null
|
|
||||||
val length = content.length
|
|
||||||
for (index in 0 until length) {
|
|
||||||
val character = content[index]
|
|
||||||
|
|
||||||
// If there are more than one or two, switch to a map
|
|
||||||
if (character == SOFT_HYPHEN) {
|
|
||||||
if (!replacing) {
|
|
||||||
replacing = true
|
|
||||||
builder = SpannableStringBuilder(content, 0, index)
|
|
||||||
}
|
|
||||||
builder!!.append(ASCII_HYPHEN)
|
|
||||||
} else if (replacing) {
|
|
||||||
builder!!.append(character)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (replacing) builder else content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.keylesspalace.tusky.entity.SearchResult
|
import com.keylesspalace.tusky.entity.SearchResult
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
@ -70,7 +69,7 @@ class BottomSheetActivityTest {
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = SpannedString("omgwat"),
|
content = "omgwat",
|
||||||
createdAt = Date(),
|
createdAt = Date(),
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
|
|
|
@ -17,7 +17,6 @@ package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Looper.getMainLooper
|
import android.os.Looper.getMainLooper
|
||||||
import android.text.SpannedString
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
@ -469,7 +468,7 @@ class ComposeActivityTest {
|
||||||
"admin",
|
"admin",
|
||||||
"admin",
|
"admin",
|
||||||
"admin",
|
"admin",
|
||||||
SpannedString(""),
|
"",
|
||||||
"https://example.token",
|
"https://example.token",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
|
@ -162,7 +161,7 @@ class FilterTest {
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = SpannedString(content),
|
content = content,
|
||||||
createdAt = Date(),
|
createdAt = Date(),
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
|
@ -39,9 +37,7 @@ class StatusComparisonTest {
|
||||||
assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gson = GsonBuilder().registerTypeAdapter(
|
private val gson = Gson()
|
||||||
Spanned::class.java, SpannedTypeAdapter()
|
|
||||||
).create()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `two equal status view data - should be equal`() {
|
fun `two equal status view data - should be equal`() {
|
||||||
|
@ -49,14 +45,12 @@ class StatusComparisonTest {
|
||||||
status = createStatus(),
|
status = createStatus(),
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
val viewdata2 = StatusViewData.Concrete(
|
val viewdata2 = StatusViewData.Concrete(
|
||||||
status = createStatus(),
|
status = createStatus(),
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
assertEquals(viewdata1, viewdata2)
|
assertEquals(viewdata1, viewdata2)
|
||||||
|
@ -68,14 +62,12 @@ class StatusComparisonTest {
|
||||||
status = createStatus(),
|
status = createStatus(),
|
||||||
isExpanded = true,
|
isExpanded = true,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
val viewdata2 = StatusViewData.Concrete(
|
val viewdata2 = StatusViewData.Concrete(
|
||||||
status = createStatus(),
|
status = createStatus(),
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
assertNotEquals(viewdata1, viewdata2)
|
assertNotEquals(viewdata1, viewdata2)
|
||||||
|
@ -87,14 +79,12 @@ class StatusComparisonTest {
|
||||||
status = createStatus(content = "whatever"),
|
status = createStatus(content = "whatever"),
|
||||||
isExpanded = true,
|
isExpanded = true,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
val viewdata2 = StatusViewData.Concrete(
|
val viewdata2 = StatusViewData.Concrete(
|
||||||
status = createStatus(),
|
status = createStatus(),
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
)
|
)
|
||||||
assertNotEquals(viewdata1, viewdata2)
|
assertNotEquals(viewdata1, viewdata2)
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@Config(sdk = [28])
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
class NetworkTimelinePagingSourceTest {
|
class NetworkTimelinePagingSourceTest {
|
||||||
|
|
||||||
private val status = mockStatusViewData()
|
private val status = mockStatusViewData()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import android.text.SpannedString
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status(
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = SpannedString("Test"),
|
content = "Test",
|
||||||
createdAt = fixedDate,
|
createdAt = fixedDate,
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 1,
|
reblogsCount = 1,
|
||||||
|
@ -50,7 +49,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete(
|
||||||
status = mockStatus(id),
|
status = mockStatus(id),
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isShowingContent = false,
|
isShowingContent = false,
|
||||||
isCollapsible = false,
|
|
||||||
isCollapsed = true,
|
isCollapsed = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue