diff --git a/app/build.gradle b/app/build.gradle
index a6ceea876..9805d9670 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -95,7 +95,7 @@ android {
}
}
-ext.coroutinesVersion = "1.6.0"
+ext.coroutinesVersion = "1.6.1"
ext.lifecycleVersion = "2.4.1"
ext.roomVersion = '2.4.2'
ext.retrofitVersion = '2.9.0'
@@ -112,8 +112,6 @@ repositories {
// if libraries are changed here, they should also be changed in LicenseActivity
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
-
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
@@ -150,6 +148,7 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
+ implementation "at.connyduck:kotlin-result-calladapter:1.0.1"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
@@ -189,8 +188,8 @@ dependencies {
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "org.robolectric:robolectric:4.4"
- testImplementation "org.mockito:mockito-inline:3.6.28"
- testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
+ testImplementation "org.mockito:mockito-inline:4.4.0"
+ testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json
new file mode 100644
index 000000000..97ad414e0
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json
@@ -0,0 +1,815 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 32,
+ "identityHash": "c92343960c9d46d9cfd49f1873cce47d",
+ "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_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
+ "fields": [
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accounts",
+ "columnName": "accounts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unread",
+ "columnName": "unread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.id",
+ "columnName": "s_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.url",
+ "columnName": "s_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.inReplyToId",
+ "columnName": "s_inReplyToId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.inReplyToAccountId",
+ "columnName": "s_inReplyToAccountId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.account",
+ "columnName": "s_account",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.content",
+ "columnName": "s_content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.createdAt",
+ "columnName": "s_createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.emojis",
+ "columnName": "s_emojis",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.favouritesCount",
+ "columnName": "s_favouritesCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.favourited",
+ "columnName": "s_favourited",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.bookmarked",
+ "columnName": "s_bookmarked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.sensitive",
+ "columnName": "s_sensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.spoilerText",
+ "columnName": "s_spoilerText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.attachments",
+ "columnName": "s_attachments",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.mentions",
+ "columnName": "s_mentions",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.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.collapsible",
+ "columnName": "s_collapsible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.collapsed",
+ "columnName": "s_collapsed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.muted",
+ "columnName": "s_muted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.poll",
+ "columnName": "s_poll",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id",
+ "accountId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c92343960c9d46d9cfd49f1873cce47d')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
new file mode 100644
index 000000000..e6d8ec7dc
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 46654b625..271eb2c16 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,22 @@
android:theme="@style/TuskyTheme"
android:usesCleartextTraffic="false">
+
+
+
+
+
+
+
+
+
+
+
@@ -30,13 +46,7 @@
-
-
-
-
-
@@ -84,9 +94,6 @@
-
- onFetchUserInfoSuccess(userInfo)
- },
- { throwable ->
- Log.e(TAG, "Failed to fetch user info. " + throwable.message)
- }
- )
+ private fun fetchUserInfo() = lifecycleScope.launch {
+ mastodonApi.accountVerifyCredentials().fold(
+ { userInfo ->
+ onFetchUserInfoSuccess(userInfo)
+ },
+ { throwable ->
+ Log.e(TAG, "Failed to fetch user info. " + throwable.message)
+ }
+ )
}
private fun onFetchUserInfoSuccess(me: Account) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt
new file mode 100644
index 000000000..7f621f9b5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt
@@ -0,0 +1,54 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.keylesspalace.tusky.components.login.LoginActivity
+import com.keylesspalace.tusky.components.notifications.NotificationHelper
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.di.Injectable
+import net.accelf.yuito.CustomUncaughtExceptionHandler
+import javax.inject.Inject
+
+@SuppressLint("CustomSplashScreen")
+class SplashActivity : AppCompatActivity(), Injectable {
+
+ @Inject
+ lateinit var accountManager: AccountManager
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(applicationContext))
+
+ /** delete old notification channels */
+ NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
+
+ /** Determine whether the user is currently logged in, and if so go ahead and load the
+ * timeline. Otherwise, start the activity_login screen. */
+
+ val intent = if (accountManager.activeAccount != null) {
+ Intent(this, MainActivity::class.java)
+ } else {
+ LoginActivity.getIntent(this, false)
+ }
+ startActivity(intent)
+ finish()
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
index 64d29577b..fda2c82b6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
@@ -283,7 +283,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
return@fromCallable false
}
-
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose {
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
index 68f921013..747ce4bfb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
@@ -41,6 +41,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
+import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status;
@@ -229,7 +230,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_FOLLOW: {
if (payloadForHolder == null) {
FollowViewHolder holder = (FollowViewHolder) viewHolder;
- holder.setMessage(concreteNotificaton.getAccount());
+ holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP);
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
}
break;
@@ -287,7 +288,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case REBLOG: {
return VIEW_TYPE_STATUS_NOTIFICATION;
}
- case FOLLOW: {
+ case FOLLOW:
+ case SIGN_UP: {
return VIEW_TYPE_FOLLOW;
}
case FOLLOW_REQUEST: {
@@ -339,10 +341,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.statusDisplayOptions = statusDisplayOptions;
}
- void setMessage(TimelineAccount account) {
+ void setMessage(TimelineAccount account, Boolean isSignUp) {
Context context = message.getContext();
- String format = context.getString(R.string.notification_follow_format);
+ String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format);
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
String wholeMessage = String.format(format, wrappedDisplayName);
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
@@ -599,13 +601,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
avatarRadius24dp, statusDisplayOptions.animateAvatars());
}
- private void setQuoteContainer(Status status, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) {
- if (status != null) {
+ private void setQuoteContainer(StatusViewData.Concrete quote, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) {
+ if (quote != null) {
quoteContainer.setVisibility(View.VISIBLE);
- new QuoteInlineHelper(status, quoteContainer, listener,
+ ViewQuoteInlineBinding binding = ViewQuoteInlineBinding.bind(quoteContainer);
+ new QuoteInlineHelper(binding, listener,
quoteContainer.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp),
statusDisplayOptions)
- .setupQuoteContainer();
+ .setupQuoteContainer(quote);
} else {
quoteContainer.setVisibility(View.GONE);
}
@@ -678,7 +681,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
- setQuoteContainer(statusViewData.getStatus().getQuote(), listener, statusDisplayOptions);
+ setQuoteContainer(statusViewData.getQuoteViewData(), listener, statusDisplayOptions);
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
index 2b37289f1..83411d671 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
@@ -33,6 +33,7 @@ import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.google.android.material.button.MaterialButton;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewMediaActivity;
+import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData;
@@ -480,10 +481,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
favouriteButton.setChecked(favourited);
}
- private void setQuoteContainer(Status status, final StatusActionListener listener, StatusDisplayOptions statusDisplayOptions) {
- if (status != null) {
+ private void setQuoteContainer(StatusViewData.Concrete quote, final StatusActionListener listener, StatusDisplayOptions statusDisplayOptions) {
+ if (quote != null) {
quoteContainer.setVisibility(View.VISIBLE);
- new QuoteInlineHelper(status, quoteContainer, listener, avatarRadius24dp, statusDisplayOptions).setupQuoteContainer();
+ ViewQuoteInlineBinding binding = ViewQuoteInlineBinding.bind(quoteContainer);
+ new QuoteInlineHelper(binding, listener, avatarRadius24dp, statusDisplayOptions).setupQuoteContainer(quote);
} else {
quoteContainer.setVisibility(View.GONE);
}
@@ -857,7 +859,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
actionable.getAccount().getBot(), statusDisplayOptions);
setReblogged(actionable.getReblogged());
setFavourited(actionable.getFavourited());
- setQuoteContainer(actionable.getQuote(), listener, statusDisplayOptions);
+ setQuoteContainer(status.getQuoteViewData(), listener, statusDisplayOptions);
setBookmarked(actionable.getBookmarked());
List attachments = actionable.getAttachments();
boolean sensitive = actionable.getSensitive();
@@ -1152,9 +1154,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener
) {
- final Card card = status.getActionable().getCard();
+ final Status actionable = status.getActionable();
+ final Card card = actionable.getCard();
if (cardViewMode != CardViewMode.NONE &&
- status.getActionable().getAttachments().size() == 0 &&
+ actionable.getAttachments().size() == 0 &&
+ actionable.getPoll() == null &&
card != null &&
!TextUtils.isEmpty(card.getUrl()) &&
(!status.isCollapsible() || !status.isCollapsed())) {
@@ -1176,7 +1180,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
// so let's blur the preview in that case
// If media previews are disabled, show placeholder for cards as well
- if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) {
+ if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
int topLeftRadius = 0;
int topRightRadius = 0;
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
index 12b2fa35b..33987604f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
@@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDomain
import com.keylesspalace.tusky.util.hide
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.show
import com.keylesspalace.tusky.util.viewBinding
@@ -380,12 +380,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
}
}
viewModel.accountFieldData.observe(
- this,
- {
- accountFieldAdapter.fields = it
- accountFieldAdapter.notifyDataSetChanged()
- }
- )
+ this
+ ) {
+ accountFieldAdapter.fields = it
+ accountFieldAdapter.notifyDataSetChanged()
+ }
viewModel.noteSaved.observe(this) {
binding.saveNoteInfo.visible(it, View.INVISIBLE)
}
@@ -400,11 +399,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
adapter.refreshContent()
}
viewModel.isRefreshing.observe(
- this,
- { isRefreshing ->
- binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
- }
- )
+ this
+ ) { isRefreshing ->
+ binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
+ }
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
@@ -415,7 +413,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountUsernameTextView.text = usernameFormatted
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)
// accountFieldAdapter.fields = account.fields ?: emptyList()
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt
index 093dbcfb5..d51bb1452 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt
@@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.createClickableText
import com.keylesspalace.tusky.util.emojify
+import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.setClickableText
class AccountFieldAdapter(
@@ -65,7 +66,7 @@ class AccountFieldAdapter(
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
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)
if (field.verifiedAt != null) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
index d1ae0b9e3..10dc303f6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
@@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.rx3.rxSingle
import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor(
@@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map> { Either.Left(it) }
.onErrorResumeNext {
- mastodonApi.getInstance()
- .map { Either.Right(it) }
+ rxSingle {
+ mastodonApi.getInstance().getOrThrow()
+ }.map { Either.Right(it) }
}
) { emojis, either ->
either.asLeftOrNull()?.copy(emojiList = emojis)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
index 0764a03df..dc1253e80 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
@@ -48,7 +48,8 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.launch
-import java.util.*
+import kotlinx.coroutines.rx3.rxSingle
+import java.util.Locale
import javax.inject.Inject
class ComposeViewModel @Inject constructor(
@@ -110,7 +111,7 @@ class ComposeViewModel @Inject constructor(
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
when (loadActually) {
true -> Single.zip(
- api.getCustomEmojis(), api.getInstance()
+ api.getCustomEmojis(), rxSingle { api.getInstance().getOrThrow() }
) { emojis, instance ->
InstanceEntity(
instance = accountManager.activeAccount?.domain!!,
@@ -298,7 +299,7 @@ class ComposeViewModel @Inject constructor(
): LiveData {
val deletionObservable = if (isEditingScheduledToot) {
- api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
+ rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
} else {
Observable.just(Unit)
}.toLiveData()
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt
index 89c1ad0f1..0c9465142 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt
@@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
class ConversationAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val listener: StatusActionListener
-) : PagingDataAdapter(CONVERSATION_COMPARATOR) {
+) : PagingDataAdapter(CONVERSATION_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
@@ -37,17 +37,13 @@ class ConversationAdapter(
holder.setupWithConversation(getItem(position))
}
- fun item(position: Int): ConversationEntity? {
- return getItem(position)
- }
-
companion object {
- val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
+ val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
return oldItem.id == newItem.id
}
- override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
+ override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
return oldItem == newItem
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
index a847fe749..19e8ba041 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
@@ -15,7 +15,6 @@
package com.keylesspalace.tusky.components.conversation
-import android.text.Spanned
import androidx.room.Embedded
import androidx.room.Entity
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.Status
import com.keylesspalace.tusky.entity.TimelineAccount
-import com.keylesspalace.tusky.util.shouldTrimStatus
+import com.keylesspalace.tusky.viewdata.StatusViewData
import java.util.Date
@Entity(primaryKeys = ["id", "accountId"])
@@ -38,7 +37,16 @@ data class ConversationEntity(
val accounts: List,
val unread: Boolean,
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
-)
+) {
+ fun toViewData(): ConversationViewData {
+ return ConversationViewData(
+ id = id,
+ accounts = accounts,
+ unread = unread,
+ lastStatus = lastStatus.toViewData()
+ )
+ }
+}
data class ConversationAccountEntity(
val id: String,
@@ -67,7 +75,7 @@ data class ConversationStatusEntity(
val inReplyToId: String?,
val inReplyToAccountId: String?,
val account: ConversationAccountEntity,
- val content: Spanned,
+ val content: String,
val createdAt: Date,
val emojis: List,
val favouritesCount: Int,
@@ -80,96 +88,44 @@ data class ConversationStatusEntity(
val tags: List?,
val showingHiddenContent: Boolean,
val expanded: Boolean,
- val collapsible: Boolean,
val collapsed: Boolean,
val muted: Boolean,
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
-
- if (id != other.id) return false
- 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,
- url = url,
- account = account.toAccount(),
- inReplyToId = inReplyToId,
- inReplyToAccountId = inReplyToAccountId,
- content = content,
- reblog = null,
- createdAt = createdAt,
- emojis = emojis,
- reblogsCount = 0,
- favouritesCount = favouritesCount,
- reblogged = false,
- favourited = favourited,
- bookmarked = bookmarked,
- sensitive = sensitive,
- spoilerText = spoilerText,
- visibility = Status.Visibility.DIRECT,
- attachments = attachments,
- mentions = mentions,
- tags = tags,
- application = null,
- pinned = false,
- muted = muted,
- poll = poll,
- card = null,
- quote = null,
+ fun toViewData(): StatusViewData.Concrete {
+ return StatusViewData.Concrete(
+ status = Status(
+ id = id,
+ url = url,
+ account = account.toAccount(),
+ inReplyToId = inReplyToId,
+ inReplyToAccountId = inReplyToAccountId,
+ content = content,
+ reblog = null,
+ createdAt = createdAt,
+ emojis = emojis,
+ reblogsCount = 0,
+ favouritesCount = favouritesCount,
+ reblogged = false,
+ favourited = favourited,
+ bookmarked = bookmarked,
+ sensitive = sensitive,
+ spoilerText = spoilerText,
+ visibility = Status.Visibility.DIRECT,
+ attachments = attachments,
+ mentions = mentions,
+ tags = tags,
+ application = null,
+ pinned = false,
+ muted = muted,
+ poll = poll,
+ card = null,
+ quote = null,
+ ),
+ isExpanded = expanded,
+ isShowingContent = showingHiddenContent,
+ isCollapsed = collapsed
)
}
}
@@ -203,7 +159,6 @@ fun Status.toEntity() =
tags = tags,
showingHiddenContent = false,
expanded = false,
- collapsible = shouldTrimStatus(content),
collapsed = true,
muted = muted ?: false,
poll = poll
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt
new file mode 100644
index 000000000..470675d17
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt
@@ -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 . */
+
+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,
+ 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
+ )
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java
index 6f95619c7..55c85fcdc 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java
@@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
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.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
+import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.List;
@@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
}
- void setupWithConversation(ConversationEntity conversation) {
- ConversationStatusEntity status = conversation.getLastStatus();
- ConversationAccountEntity account = status.getAccount();
+ void setupWithConversation(ConversationViewData conversation) {
+ StatusViewData.Concrete statusViewData = conversation.getLastStatus();
+ 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);
setUsername(account.getUsername());
@@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
List attachments = status.getAttachments();
boolean sensitive = status.getSensitive();
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
- setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
+ setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) {
@@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
mediaLabel.setVisibility(View.GONE);
}
} else {
- setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent());
+ setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
// Hide all unused views.
mediaPreviews[0].setVisibility(View.GONE);
mediaPreviews[1].setVisibility(View.GONE);
@@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
hideSensitiveMediaWarning();
}
- setupButtons(listener, account.getId(), status.getContent().toString(),
+ setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
false, statusDisplayOptions);
- setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
+ setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
status.getMentions(), status.getTags(), status.getEmojis(),
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
index 0859799fb..08f97b5bd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
@@ -104,7 +104,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
initSwipeToRefresh()
- lifecycleScope.launch {
+ viewLifecycleOwner.lifecycleScope.launch {
viewModel.conversationFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
@@ -155,7 +155,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onFavourite(favourite: Boolean, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.favourite(favourite, conversation)
}
}
@@ -165,18 +165,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onBookmark(favourite: Boolean, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.bookmark(favourite, conversation)
}
}
override fun onMore(view: View, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
val popup = PopupMenu(requireContext(), view)
popup.inflate(R.menu.conversation_more)
- if (conversation.lastStatus.muted) {
+ if (conversation.lastStatus.status.muted == true) {
popup.menu.removeItem(R.id.status_mute_conversation)
} else {
popup.menu.removeItem(R.id.status_unmute_conversation)
@@ -195,14 +195,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
- adapter.item(position)?.let { conversation ->
- viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
+ adapter.peek(position)?.let { conversation ->
+ viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
}
}
override fun onViewThread(position: Int) {
- adapter.item(position)?.let { conversation ->
- viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
+ adapter.peek(position)?.let { conversation ->
+ viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
}
}
@@ -211,13 +211,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onExpandedChange(expanded: Boolean, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.expandHiddenStatus(expanded, conversation)
}
}
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.showContent(isShowing, conversation)
}
}
@@ -227,7 +227,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.collapseLongStatus(isCollapsed, conversation)
}
}
@@ -247,12 +247,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onReply(position: Int) {
- adapter.item(position)?.let { conversation ->
- reply(conversation.lastStatus.toStatus())
+ adapter.peek(position)?.let { conversation ->
+ reply(conversation.lastStatus.status)
}
}
- private fun deleteConversation(conversation: ConversationEntity) {
+ private fun deleteConversation(conversation: ConversationViewData) {
AlertDialog.Builder(requireContext())
.setMessage(R.string.dialog_delete_conversation_warning)
.setNegativeButton(android.R.string.cancel, null)
@@ -274,7 +274,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onVoteInPoll(position: Int, choices: MutableList) {
- adapter.item(position)?.let { conversation ->
+ adapter.peek(position)?.let { conversation ->
viewModel.voteInPoll(choices, conversation)
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
index 396f8e486..9326a05c0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
@@ -16,16 +16,18 @@
package com.keylesspalace.tusky.components.conversation
import android.util.Log
+import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
+import androidx.paging.map
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
-import com.keylesspalace.tusky.util.RxAwareViewModel
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import javax.inject.Inject
@@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor(
private val database: AppDatabase,
private val accountManager: AccountManager,
private val api: MastodonApi
-) : RxAwareViewModel() {
+) : ViewModel() {
@OptIn(ExperimentalPagingApi::class)
val conversationFlow = Pager(
@@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor(
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
)
.flow
+ .map { pagingData ->
+ pagingData.map { conversation -> conversation.toViewData() }
+ }
.cachedIn(viewModelScope)
- fun favourite(favourite: Boolean, conversation: ConversationEntity) {
+ fun favourite(favourite: Boolean, conversation: ConversationViewData) {
viewModelScope.launch {
try {
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(favourited = favourite)
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ favourited = favourite
)
- database.conversationDao().insert(newConversation)
+ saveConversationToDb(newConversation)
} catch (e: Exception) {
Log.w(TAG, "failed to favourite status", e)
}
}
}
- fun bookmark(bookmark: Boolean, conversation: ConversationEntity) {
+ fun bookmark(bookmark: Boolean, conversation: ConversationViewData) {
viewModelScope.launch {
try {
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ bookmarked = bookmark
)
- database.conversationDao().insert(newConversation)
+ saveConversationToDb(newConversation)
} catch (e: Exception) {
Log.w(TAG, "failed to bookmark status", e)
}
}
}
- fun voteInPoll(choices: List, conversation: ConversationEntity) {
+ fun voteInPoll(choices: List, conversation: ConversationViewData) {
viewModelScope.launch {
try {
- val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(poll = poll)
+ val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await()
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ poll = poll
)
- database.conversationDao().insert(newConversation)
+ saveConversationToDb(newConversation)
} catch (e: Exception) {
Log.w(TAG, "failed to vote in poll", e)
}
}
}
- fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
+ fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) {
viewModelScope.launch {
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(expanded = expanded)
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ expanded = expanded
)
saveConversationToDb(newConversation)
}
}
- fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
+ fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) {
viewModelScope.launch {
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ collapsed = collapsed
)
saveConversationToDb(newConversation)
}
}
- fun showContent(showing: Boolean, conversation: ConversationEntity) {
+ fun showContent(showing: Boolean, conversation: ConversationViewData) {
viewModelScope.launch {
- val newConversation = conversation.copy(
- lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ showingHiddenContent = showing
)
saveConversationToDb(newConversation)
}
}
- fun remove(conversation: ConversationEntity) {
+ fun remove(conversation: ConversationViewData) {
viewModelScope.launch {
try {
api.deleteConversation(conversationId = conversation.id)
- database.conversationDao().delete(conversation)
+ database.conversationDao().delete(
+ id = conversation.id,
+ accountId = accountManager.activeAccount!!.id
+ )
} catch (e: Exception) {
Log.w(TAG, "failed to delete conversation", e)
}
}
}
- fun muteConversation(conversation: ConversationEntity) {
+ fun muteConversation(conversation: ConversationViewData) {
viewModelScope.launch {
try {
- val newStatus = timelineCases.muteConversation(
+ timelineCases.muteConversation(
conversation.lastStatus.id,
- !conversation.lastStatus.muted
+ !(conversation.lastStatus.status.muted ?: false)
).await()
- val newConversation = conversation.copy(
- lastStatus = newStatus.toEntity()
+ val newConversation = conversation.toEntity(
+ accountId = accountManager.activeAccount!!.id,
+ muted = !(conversation.lastStatus.status.muted ?: false)
)
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)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
index a95e1cf8b..dc3c16962 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
@@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
import com.keylesspalace.tusky.di.Injectable
-import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.viewBinding
@@ -159,32 +158,33 @@ class LoginActivity : BaseActivity(), Injectable {
setLoading(true)
lifecycleScope.launch {
- val credentials: AppCredentials = try {
- mastodonApi.authenticateApp(
- domain, getString(R.string.app_name), oauthRedirectUri,
- OAUTH_SCOPES, getString(R.string.tusky_website)
- )
- } catch (e: Exception) {
- binding.loginButton.isEnabled = true
- binding.domainTextInputLayout.error =
- getString(R.string.error_failed_app_registration)
- setLoading(false)
- Log.e(TAG, Log.getStackTraceString(e))
- return@launch
- }
+ mastodonApi.authenticateApp(
+ domain, getString(R.string.app_name), oauthRedirectUri,
+ OAUTH_SCOPES, getString(R.string.tusky_website)
+ ).fold(
+ { credentials ->
+ // Before we open browser page we save the data.
+ // Even if we don't open other apps user may go to password manager or somewhere else
+ // and we will need to pick up the process where we left off.
+ // Alternatively we could pass it all as part of the intent and receive it back
+ // but it is a bit of a workaround.
+ preferences.edit()
+ .putString(DOMAIN, domain)
+ .putString(CLIENT_ID, credentials.clientId)
+ .putString(CLIENT_SECRET, credentials.clientSecret)
+ .apply()
- // Before we open browser page we save the data.
- // Even if we don't open other apps user may go to password manager or somewhere else
- // and we will need to pick up the process where we left off.
- // Alternatively we could pass it all as part of the intent and receive it back
- // but it is a bit of a workaround.
- preferences.edit()
- .putString(DOMAIN, domain)
- .putString(CLIENT_ID, credentials.clientId)
- .putString(CLIENT_SECRET, credentials.clientSecret)
- .apply()
-
- redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
+ redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
+ },
+ { e ->
+ binding.loginButton.isEnabled = true
+ binding.domainTextInputLayout.error =
+ getString(R.string.error_failed_app_registration)
+ setLoading(false)
+ Log.e(TAG, Log.getStackTraceString(e))
+ return@launch
+ }
+ )
}
}
@@ -217,29 +217,28 @@ class LoginActivity : BaseActivity(), Injectable {
setLoading(true)
- val accessToken = try {
- mastodonApi.fetchOAuthToken(
- domain, clientId, clientSecret, oauthRedirectUri, code,
- "authorization_code"
- )
- } catch (e: Exception) {
- setLoading(false)
- binding.domainTextInputLayout.error =
- getString(R.string.error_retrieving_oauth_token)
- Log.e(
- TAG,
- "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
- )
- return
- }
+ mastodonApi.fetchOAuthToken(
+ domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
+ ).fold(
+ { accessToken ->
+ accountManager.addAccount(accessToken.accessToken, domain)
- accountManager.addAccount(accessToken.accessToken, domain)
-
- val intent = Intent(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- startActivity(intent)
- finish()
- overridePendingTransition(R.anim.explode, R.anim.explode)
+ val intent = Intent(this, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ finish()
+ overridePendingTransition(R.anim.explode, R.anim.explode)
+ },
+ { e ->
+ setLoading(false)
+ binding.domainTextInputLayout.error =
+ getString(R.string.error_retrieving_oauth_token)
+ Log.e(
+ TAG,
+ "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
+ )
+ }
+ )
}
private fun setLoading(loadingState: Boolean) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt
index 01f6c3b0e..827b56208 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt
@@ -16,6 +16,7 @@ import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.net.toUri
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.databinding.LoginWebviewBinding
@@ -103,8 +104,8 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
webView.webViewClient = object : WebViewClient() {
override fun onReceivedError(
- view: WebView?,
- request: WebResourceRequest?,
+ view: WebView,
+ request: WebResourceRequest,
error: WebResourceError
) {
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
@@ -115,7 +116,17 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
view: WebView,
request: WebResourceRequest
): Boolean {
- val url = request.url
+ return shouldOverrideUrlLoading(request.url)
+ }
+
+ /* overriding this deprecated method is necessary for it to work on api levels < 24 */
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean {
+ val url = urlString?.toUri() ?: return false
+ return shouldOverrideUrlLoading(url)
+ }
+
+ fun shouldOverrideUrlLoading(url: Uri): Boolean {
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
val error = url.getQueryParameter("error")
if (error != null) {
@@ -130,6 +141,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
}
}
}
+
webView.setBackgroundColor(Color.TRANSPARENT)
if (savedInstanceState == null) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
index 6b9afce1d..83682ab28 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
@@ -16,6 +16,9 @@
package com.keylesspalace.tusky.components.notifications;
+import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
+import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
+
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
@@ -73,8 +76,6 @@ import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
-import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
-
public class NotificationHelper {
private static int notificationId = 0;
@@ -116,6 +117,7 @@ public class NotificationHelper {
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL";
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
+ public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
/**
* WorkManager Tag
@@ -340,7 +342,7 @@ public class NotificationHelper {
Status status = body.getStatus();
String citedLocalAuthor = status.getAccount().getLocalUsername();
- String citedText = status.getContent().toString();
+ String citedText = parseAsMastodonHtml(status.getContent()).toString();
String inReplyToId = status.getId();
Status actionableStatus = status.getActionableStatus();
Status.Visibility replyVisibility = actionableStatus.getVisibility();
@@ -392,6 +394,7 @@ public class NotificationHelper {
CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(),
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
+ CHANNEL_SIGN_UP + account.getIdentifier(),
};
int[] channelNames = {
R.string.notification_mention_name,
@@ -401,6 +404,7 @@ public class NotificationHelper {
R.string.notification_favourite_name,
R.string.notification_poll_name,
R.string.notification_subscription_name,
+ R.string.notification_sign_up_name,
};
int[] channelDescriptions = {
R.string.notification_mention_descriptions,
@@ -410,6 +414,7 @@ public class NotificationHelper {
R.string.notification_favourite_description,
R.string.notification_poll_description,
R.string.notification_subscription_description,
+ R.string.notification_sign_up_description,
};
List channels = new ArrayList<>(6);
@@ -560,6 +565,8 @@ public class NotificationHelper {
return account.getNotificationsFavorited();
case POLL:
return account.getNotificationsPolls();
+ case SIGN_UP:
+ return account.getNotificationsSignUps();
default:
return false;
}
@@ -582,6 +589,8 @@ public class NotificationHelper {
return CHANNEL_FAVOURITE + account.getIdentifier();
case POLL:
return CHANNEL_POLL + account.getIdentifier();
+ case SIGN_UP:
+ return CHANNEL_SIGN_UP + account.getIdentifier();
default:
return null;
}
@@ -663,6 +672,8 @@ public class NotificationHelper {
} else {
return context.getString(R.string.poll_ended_voted);
}
+ case SIGN_UP:
+ return String.format(context.getString(R.string.notification_sign_up_format), accountName);
}
return null;
}
@@ -671,6 +682,7 @@ public class NotificationHelper {
switch (notification.getType()) {
case FOLLOW:
case FOLLOW_REQUEST:
+ case SIGN_UP:
return "@" + notification.getAccount().getUsername();
case MENTION:
case FAVOURITE:
@@ -679,13 +691,13 @@ public class NotificationHelper {
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
return notification.getStatus().getSpoilerText();
} else {
- return notification.getStatus().getContent().toString();
+ return parseAsMastodonHtml(notification.getStatus().getContent()).toString();
}
case POLL:
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
return notification.getStatus().getSpoilerText();
} else {
- StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
+ StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent()));
builder.append('\n');
Poll poll = notification.getStatus().getPoll();
List options = poll.getOptions();
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
index 2c565a710..47cb37ae7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
@@ -13,8 +13,8 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceManager
-import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
@@ -216,7 +216,7 @@ class EmojiPreference(
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
- val launchIntent = Intent(context, MainActivity::class.java)
+ val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
index 4d8ba84f3..82ee0a384 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
@@ -122,6 +122,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true
}
}
+
+ switchPreference {
+ setTitle(R.string.pref_title_notification_filter_sign_ups)
+ key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS
+ isIconSpaceReserved = false
+ isChecked = activeAccount.notificationsSignUps
+ setOnPreferenceChangeListener { _, newValue ->
+ updateAccount { it.notificationsSignUps = newValue as Boolean }
+ true
+ }
+ }
}
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
index f8991282d..9f99da530 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
@@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
+import androidx.paging.map
import com.keylesspalace.tusky.appstore.BlockEvent
import com.keylesspalace.tusky.appstore.EventHub
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.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
+import com.keylesspalace.tusky.util.toViewData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -74,6 +77,11 @@ class ReportViewModel @Inject constructor(
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
).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)
private val selectedIds = HashSet()
@@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor(
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
- val muting = relationship?.muting == true
+ val muting = relationship.muting
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId))
@@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor(
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
- val blocking = relationship?.blocking == true
+ val blocking = relationship.blocking
blockStateMutable.value = Success(blocking)
if (blocking) {
eventHub.dispatch(BlockEvent(accountId))
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
index df601efe9..3c022d3b8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
@@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions
import com.keylesspalace.tusky.util.setClickableText
import com.keylesspalace.tusky.util.shouldTrimStatus
import com.keylesspalace.tusky.util.show
+import com.keylesspalace.tusky.viewdata.StatusViewData
import com.keylesspalace.tusky.viewdata.toViewData
import java.util.Date
@@ -45,20 +46,21 @@ class StatusViewHolder(
private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler,
- private val getStatusForPosition: (Int) -> Status?
+ private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
) : RecyclerView.ViewHolder(binding.root) {
+
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val statusViewHelper = StatusViewHelper(itemView)
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
override fun onViewMedia(v: View?, idx: Int) {
- status()?.let { status ->
- adapterHandler.showMedia(v, status, idx)
+ viewdata()?.let { viewdata ->
+ adapterHandler.showMedia(v, viewdata.status, idx)
}
}
override fun onContentHiddenChange(isShowing: Boolean) {
- status()?.id?.let { id ->
+ viewdata()?.id?.let { id ->
viewState.setMediaShow(id, isShowing)
}
}
@@ -66,57 +68,57 @@ class StatusViewHolder(
init {
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
- status()?.let { status ->
- adapterHandler.setStatusChecked(status, isChecked)
+ viewdata()?.let { viewdata ->
+ adapterHandler.setStatusChecked(viewdata.status, isChecked)
}
}
binding.statusMediaPreviewContainer.clipToOutline = true
}
- fun bind(status: Status) {
- binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
+ fun bind(viewData: StatusViewData.Concrete) {
+ binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id)
updateTextView()
- val sensitive = status.sensitive
+ val sensitive = viewData.status.sensitive
statusViewHelper.setMediasPreview(
- statusDisplayOptions, status.attachments,
- sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
+ statusDisplayOptions, viewData.status.attachments,
+ sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive),
mediaViewHeight
)
- statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
- setCreatedAt(status.createdAt)
+ statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
+ setCreatedAt(viewData.status.createdAt)
}
private fun updateTextView() {
- status()?.let { status ->
+ viewdata()?.let { viewdata ->
setupCollapsedState(
- shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
- viewState.isContentShow(status.id, status.sensitive), status.spoilerText
+ shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true),
+ viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText
)
- if (status.spoilerText.isBlank()) {
- setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
+ if (viewdata.spoilerText.isBlank()) {
+ setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
binding.statusContentWarningButton.hide()
binding.statusContentWarningDescription.hide()
} 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.show()
binding.statusContentWarningButton.show()
- setContentWarningButtonText(viewState.isContentShow(status.id, true))
+ setContentWarningButtonText(viewState.isContentShow(viewdata.id, true))
binding.statusContentWarningButton.setOnClickListener {
- status()?.let { status ->
- val contentShown = viewState.isContentShow(status.id, true)
+ viewdata()?.let { viewdata ->
+ val contentShown = viewState.isContentShow(viewdata.id, true)
binding.statusContentWarningDescription.invalidate()
- viewState.setContentShow(status.id, !contentShown)
- setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
+ viewState.setContentShow(viewdata.id, !contentShown)
+ setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
setContentWarningButtonText(!contentShown)
}
}
- setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
+ setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
}
}
}
@@ -170,8 +172,8 @@ class StatusViewHolder(
/* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
binding.buttonToggleContent.setOnClickListener {
- status()?.let { status ->
- viewState.setCollapsed(status.id, !collapsed)
+ viewdata()?.let { viewdata ->
+ viewState.setCollapsed(viewdata.id, !collapsed)
updateTextView()
}
}
@@ -190,5 +192,5 @@ class StatusViewHolder(
}
}
- private fun status() = getStatusForPosition(bindingAdapterPosition)
+ private fun viewdata() = getStatusForPosition(bindingAdapterPosition)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
index 76ed2ebea..314513eb9 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
@@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
-import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
+import com.keylesspalace.tusky.viewdata.StatusViewData
class StatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler
-) : PagingDataAdapter(STATUS_COMPARATOR) {
+) : PagingDataAdapter(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
}
@@ -50,11 +50,11 @@ class StatusesAdapter(
}
companion object {
- val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() {
- override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
+ val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() {
+ override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
oldItem == newItem
- override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
+ override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
oldItem.id == newItem.id
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt
index cd3e5ac0c..766ed44ab 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt
@@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch
-import kotlinx.coroutines.rx3.await
import javax.inject.Inject
class ScheduledStatusViewModel @Inject constructor(
@@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
fun deleteScheduledStatus(status: ScheduledStatus) {
viewModelScope.launch {
- try {
- mastodonApi.deleteScheduledStatus(status.id).await()
- pagingSourceFactory.remove(status)
- } catch (throwable: Throwable) {
- Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
- }
+ mastodonApi.deleteScheduledStatus(status.id).fold(
+ {
+ pagingSourceFactory.remove(status)
+ },
+ { throwable ->
+ Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
+ }
+ )
}
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
index b62081760..79512e07b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
@@ -15,9 +15,6 @@
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.reflect.TypeToken
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.Status
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 java.util.Date
@@ -119,7 +114,7 @@ fun Status.toEntity(
authorServerId = actionableStatus.account.id,
inReplyToId = actionableStatus.inReplyToId,
inReplyToAccountId = actionableStatus.inReplyToAccountId,
- content = actionableStatus.content.toHtml(),
+ content = actionableStatus.content,
createdAt = actionableStatus.createdAt.time,
emojis = actionableStatus.emojis.let(gson::toJson),
reblogsCount = actionableStatus.reblogsCount,
@@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId,
reblog = null,
- content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
- ?: SpannedString(""),
+ content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
emojis = emojis,
reblogsCount = status.reblogsCount,
@@ -196,7 +190,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
inReplyToId = null,
inReplyToAccountId = null,
reblog = reblog,
- content = SpannedString(""),
+ content = "",
createdAt = Date(status.createdAt), // lie but whatever?
emojis = listOf(),
reblogsCount = 0,
@@ -225,8 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId,
reblog = null,
- content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
- ?: SpannedString(""),
+ content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
emojis = emojis,
reblogsCount = status.reblogsCount,
@@ -252,7 +245,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
status = status,
isExpanded = this.status.expanded,
isShowingContent = this.status.contentShowing,
- isCollapsible = shouldTrimStatus(status.content),
isCollapsed = this.status.contentCollapsed
)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
index f293ef0f2..b945debeb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
@@ -43,7 +43,10 @@ import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.viewdata.StatusViewData
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
@@ -82,15 +85,13 @@ class CachedTimelineViewModel @Inject constructor(
}
).flow
.map { pagingData ->
- pagingData.map { timelineStatus ->
+ pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
timelineStatus.toViewData(gson)
- }
- }
- .map { pagingData ->
- pagingData.filter { statusViewData ->
+ }.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
!shouldFilterStatus(statusViewData)
}
}
+ .flowOn(Dispatchers.Default)
.cachedIn(viewModelScope)
init {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt
index 64236bfa0..0685a92eb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt
@@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan
import com.keylesspalace.tusky.util.isLessThanOrEqual
import com.keylesspalace.tusky.util.toViewData
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.launch
import kotlinx.coroutines.rx3.await
@@ -81,10 +84,11 @@ class NetworkTimelineViewModel @Inject constructor(
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
).flow
.map { pagingData ->
- pagingData.filter { statusViewData ->
+ pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
!shouldFilterStatus(statusViewData)
}
}
+ .flowOn(Dispatchers.Default)
.cachedIn(viewModelScope)
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
index 0c25cbbc9..5da91e201 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
@@ -50,6 +50,7 @@ data class AccountEntity(
var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true,
var notificationsSubscriptions: Boolean = true,
+ var notificationsSignUps: Boolean = true,
var notificationSound: Boolean = true,
var notificationVibration: Boolean = true,
var notificationLight: Boolean = true,
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
index 159a6f529..c541958ae 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
@@ -31,7 +31,7 @@ import java.io.File;
*/
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
- }, version = 31)
+ }, version = 33)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@@ -483,4 +483,48 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("DELETE FROM `TimelineStatusEntity`");
}
};
+
+ public static final Migration MIGRATION_31_32 = new Migration(31, 32) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ 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`))");
+ }
+ };
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt
index 393a23925..fe093bd0c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt
@@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db
import androidx.paging.PagingSource
import androidx.room.Dao
-import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@@ -31,8 +30,8 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(conversation: ConversationEntity): Long
- @Delete
- suspend fun delete(conversation: ConversationEntity): Int
+ @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId")
+ suspend fun delete(id: String, accountId: Long): Int
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
fun conversationsForAccount(accountId: Long): PagingSource
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
index ef822efe9..5efb7e628 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
@@ -15,9 +15,6 @@
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.TypeConverter
import com.google.gson.Gson
@@ -32,10 +29,8 @@ import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
-import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder
import java.net.URLEncoder
-import java.util.ArrayList
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
@@ -144,22 +139,6 @@ class Converters @Inject constructor (
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
fun pollToJson(poll: Poll?): String? {
return gson.toJson(poll)
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index b3ac85daf..abd777619 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -23,6 +23,7 @@ import com.keylesspalace.tusky.FiltersActivity
import com.keylesspalace.tusky.LicenseActivity
import com.keylesspalace.tusky.ListsActivity
import com.keylesspalace.tusky.MainActivity
+import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.ViewMediaActivity
@@ -118,6 +119,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesDraftActivity(): DraftsActivity
+ @ContributesAndroidInjector
+ abstract fun contributesSplashActivity(): SplashActivity
+
@ContributesAndroidInjector
abstract fun contributesAccessTokenLoginActivity(): AccessTokenLoginActivity
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
index 4695f63b8..326428fa8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -68,7 +68,8 @@ class AppModule {
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
- AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31
+ AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
+ AppDatabase.MIGRATION_32_33
)
.build()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
index 0488c836d..0c2458208 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
@@ -18,12 +18,10 @@ package com.keylesspalace.tusky.di
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
-import android.text.Spanned
+import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
import com.google.gson.Gson
-import com.google.gson.GsonBuilder
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AccountManager
-import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.NotestockApi
@@ -53,11 +51,7 @@ class NetworkModule {
@Provides
@Singleton
- fun providesGson(): Gson {
- return GsonBuilder()
- .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
- .create()
- }
+ fun providesGson() = Gson()
@Provides
@Singleton
@@ -114,6 +108,7 @@ class NetworkModule {
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
+ .addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
.build()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
index ab20a4e64..db01a5f9b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
@@ -15,7 +15,6 @@
package com.keylesspalace.tusky.entity
-import android.text.Spanned
import com.google.gson.annotations.SerializedName
import java.util.Date
@@ -24,7 +23,7 @@ data class Account(
@SerializedName("username") val localUsername: String,
@SerializedName("acct", alternate = ["subject"]) val username: String,
@SerializedName("display_name") private 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 avatar: String,
val header: String,
@@ -54,56 +53,6 @@ data class Account(
get() = displayName.orEmpty()
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(
@@ -115,7 +64,7 @@ data class AccountSource(
data class Field(
val name: String,
- val value: Spanned,
+ val value: String,
@SerializedName("verified_at") val verifiedAt: Date?
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
index 400e9764d..00d5659d5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
@@ -15,13 +15,12 @@
package com.keylesspalace.tusky.entity
-import android.text.Spanned
import com.google.gson.annotations.SerializedName
import java.util.Date
data class Announcement(
val id: String,
- val content: Spanned,
+ val content: String,
@SerializedName("starts_at") val startsAt: Date?,
@SerializedName("ends_at") val endsAt: Date?,
@SerializedName("all_day") val allDay: Boolean,
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
index 52011f3d1..29fe7f8ee 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
@@ -15,13 +15,12 @@
package com.keylesspalace.tusky.entity
-import android.text.Spanned
import com.google.gson.annotations.SerializedName
data class Card(
val url: String,
- val title: Spanned,
- val description: Spanned,
+ val title: String,
+ val description: String,
@SerializedName("author_name") val authorName: String,
val image: String,
val type: String,
@@ -31,9 +30,7 @@ data class Card(
val embed_url: String?
) {
- override fun hashCode(): Int {
- return url.hashCode()
- }
+ override fun hashCode() = url.hashCode()
override fun equals(other: Any?): Boolean {
if (other !is Card) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
index ae2d74a90..ddcf5e618 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
@@ -37,7 +37,9 @@ data class Notification(
FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"),
POLL("poll"),
- STATUS("status");
+ STATUS("status"),
+ SIGN_UP("admin.sign_up"),
+ ;
companion object {
@@ -49,7 +51,7 @@ data class Notification(
}
return UNKNOWN
}
- val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS)
+ val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP)
}
override fun toString(): String {
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
index d69e5d807..971d53139 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
@@ -16,10 +16,9 @@
package com.keylesspalace.tusky.entity
import android.text.SpannableStringBuilder
-import android.text.Spanned
import android.text.style.URLSpan
import com.google.gson.annotations.SerializedName
-import java.util.ArrayList
+import com.keylesspalace.tusky.util.parseAsMastodonHtml
import java.util.Date
data class Status(
@@ -29,7 +28,7 @@ data class Status(
@SerializedName("in_reply_to_id") var inReplyToId: String?,
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
val reblog: Status?,
- val content: Spanned,
+ val content: String,
@SerializedName("created_at", alternate = ["published"]) val createdAt: Date,
val emojis: List,
@SerializedName("reblogs_count") val reblogsCount: Int,
@@ -143,8 +142,9 @@ data class Status(
}
private fun getEditableText(): String {
- val builder = SpannableStringBuilder(content)
- for (span in content.getSpans(0, content.length, URLSpan::class.java)) {
+ val contentSpanned = content.parseAsMastodonHtml()
+ val builder = SpannableStringBuilder(content.parseAsMastodonHtml())
+ for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) {
val url = span.url
for ((_, url1, username) in mentions) {
if (url == url1) {
@@ -158,71 +158,6 @@ data class Status(
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(
val id: String,
val url: String,
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
index b510597cd..16ca79d44 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
@@ -15,6 +15,10 @@
package com.keylesspalace.tusky.fragment;
+import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
+import static autodispose2.AutoDispose.autoDisposable;
+import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
+
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
@@ -114,10 +118,6 @@ import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
-import static autodispose2.AutoDispose.autoDisposable;
-import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
-import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
-
public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
@@ -716,6 +716,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_poll_name);
case STATUS:
return getString(R.string.notification_subscription_name);
+ case SIGN_UP:
+ return getString(R.string.notification_sign_up_name);
default:
return "Unknown";
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt
deleted file mode 100644
index 94a05065b..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt
+++ /dev/null
@@ -1,62 +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 . */
-
-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 org.jsoup.Jsoup
-import java.lang.reflect.Type
-
-class SpannedTypeAdapter : JsonDeserializer, JsonSerializer {
- @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("
", "
")
- ?.replace("
", "
")
- ?.replace("
", "
")
- ?.replace(" ", " ")
- ?.let { html ->
- Jsoup.parse(html)
- .apply {
- select(".quote-inline").forEach { it.remove() }
- }
- .html()
- }
- ?.parseAsHtml()
- /* Html.fromHtml returns trailing whitespace if the html ends in a
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))
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
index 3ee8d4cec..111cad56c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -68,7 +68,7 @@ import retrofit2.http.Query
interface MastodonApi {
companion object {
- const val ENDPOINT_AUTHORIZE = "/oauth/authorize"
+ const val ENDPOINT_AUTHORIZE = "oauth/authorize"
const val DOMAIN_HEADER = "domain"
const val PLACEHOLDER_DOMAIN = "dummy.placeholder"
}
@@ -80,7 +80,7 @@ interface MastodonApi {
fun getCustomEmojis(): Single>
@GET("api/v1/instance")
- fun getInstance(): Single
+ suspend fun getInstance(): Result
@GET("api/v1/filters")
fun getFilters(): Single>
@@ -249,12 +249,12 @@ interface MastodonApi {
): Single>
@DELETE("api/v1/scheduled_statuses/{id}")
- fun deleteScheduledStatus(
+ suspend fun deleteScheduledStatus(
@Path("id") scheduledStatusId: String
- ): Single
+ ): Result
@GET("api/v1/accounts/verify_credentials")
- fun accountVerifyCredentials(): Single
+ suspend fun accountVerifyCredentials(): Result
@FormUrlEncoded
@PATCH("api/v1/accounts/update_credentials")
@@ -265,7 +265,7 @@ interface MastodonApi {
@Multipart
@PATCH("api/v1/accounts/update_credentials")
- fun accountUpdateCredentials(
+ suspend fun accountUpdateCredentials(
@Part(value = "display_name") displayName: RequestBody?,
@Part(value = "note") note: RequestBody?,
@Part(value = "locked") locked: RequestBody?,
@@ -279,7 +279,7 @@ interface MastodonApi {
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
- ): Call
+ ): Result
@GET("api/v1/accounts/search")
fun searchAccounts(
@@ -447,7 +447,7 @@ interface MastodonApi {
@Field("redirect_uris") redirectUris: String,
@Field("scopes") scopes: String,
@Field("website") website: String
- ): AppCredentials
+ ): Result
@FormUrlEncoded
@POST("oauth/token")
@@ -458,7 +458,7 @@ interface MastodonApi {
@Field("redirect_uri") redirectUri: String,
@Field("code") code: String,
@Field("grant_type") grantType: String
- ): AccessToken
+ ): Result
@FormUrlEncoded
@POST("api/v1/lists")
diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
index c0ef7b2f5..db2bf9cf5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
@@ -69,6 +69,7 @@ object PrefKeys {
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
+ const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt
new file mode 100644
index 000000000..12098a6ee
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt
@@ -0,0 +1,70 @@
+/* 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 . */
+
+@file:JvmName("StatusParsingHelper")
+
+package com.keylesspalace.tusky.util
+
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import androidx.core.text.parseAsHtml
+import org.jsoup.Jsoup.parse
+
+/**
+ * parse a String containing html from the Mastodon api to Spanned
+ */
+fun String.parseAsMastodonHtml(): Spanned {
+ return this.replace("
", "
")
+ .replace("
", "
")
+ .replace("
", "
")
+ .replace(" ", " ")
+ .let { parse(it) }
+ .apply {
+ select(".quote-inline").forEach { it.remove() }
+ }
+ .html()
+ .parseAsHtml()
+ /* Html.fromHtml returns trailing whitespace if the html ends in a 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 = '-'
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
index 52d9713f4..fef9c0bb8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
@@ -27,12 +27,9 @@ fun Status.toViewData(
isExpanded: Boolean,
isCollapsed: Boolean
): StatusViewData.Concrete {
- val visibleStatus = this.reblog ?: this
-
return StatusViewData.Concrete(
status = this,
isShowingContent = isShowingContent,
- isCollapsible = shouldTrimStatus(visibleStatus.content),
isCollapsed = isCollapsed,
isExpanded = isExpanded,
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt
index d8f271578..be94369c5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt
@@ -15,9 +15,11 @@
package com.keylesspalace.tusky.viewdata
import android.os.Build
-import android.text.SpannableStringBuilder
import android.text.Spanned
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.
@@ -32,13 +34,6 @@ sealed class StatusViewData {
val status: Status,
val isExpanded: 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
* 500 characters or not.
@@ -51,6 +46,14 @@ sealed class StatusViewData {
override val id: String
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 spoilerText: String
val username: String
@@ -71,48 +74,23 @@ sealed class StatusViewData {
val rebloggingStatus: Status?
get() = if (status.reblog != null) status else null
+ val quoteViewData =
+ status.quote?.let { Concrete(it, isExpanded, isShowingContent, isCollapsed) }
+
init {
if (Build.VERSION.SDK_INT == 23) {
// https://github.com/tuskyapp/Tusky/issues/563
- this.content = replaceCrashingCharacters(status.actionableStatus.content)
+ this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml())
this.spoilerText =
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
this.username =
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
} else {
- this.content = status.actionableStatus.content
+ this.content = status.actionableStatus.content.parseAsMastodonHtml()
this.spoilerText = status.actionableStatus.spoilerText
this.username = status.actionableStatus.account.username
}
- }
-
- 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
- }
+ this.isCollapsible = shouldTrimStatus(this.content)
}
/** Helper for Java */
diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt
index c3c568db3..5849582ab 100644
--- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt
@@ -20,6 +20,7 @@ import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.entity.Account
@@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.randomAlphanumericString
-import io.reactivex.rxjava3.disposables.CompositeDisposable
-import io.reactivex.rxjava3.kotlin.addTo
+import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException
import org.json.JSONObject
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
+import retrofit2.HttpException
import java.io.File
import javax.inject.Inject
@@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
private var oldProfileData: Account? = null
- private val disposables = CompositeDisposable()
-
- fun obtainProfile() {
+ fun obtainProfile() = viewModelScope.launch {
if (profileData.value == null || profileData.value is Error) {
profileData.postValue(Loading())
- mastodonApi.accountVerifyCredentials()
- .subscribe(
- { profile ->
- oldProfileData = profile
- profileData.postValue(Success(profile))
- },
- {
- profileData.postValue(Error())
- }
- )
- .addTo(disposables)
+ mastodonApi.accountVerifyCredentials().fold(
+ { profile ->
+ oldProfileData = profile
+ profileData.postValue(Success(profile))
+ },
+ {
+ profileData.postValue(Error())
+ }
+ )
}
}
@@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
return
}
- mastodonApi.accountUpdateCredentials(
- displayName, note, locked, avatar, header,
- field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
- ).enqueue(object : Callback {
- override fun onResponse(call: Call, response: Response) {
- val newProfileData = response.body()
- if (!response.isSuccessful || newProfileData == null) {
- val errorResponse = response.errorBody()?.string()
- val errorMsg = if (!errorResponse.isNullOrBlank()) {
- try {
- JSONObject(errorResponse).optString("error", null)
- } catch (e: JSONException) {
+ viewModelScope.launch {
+ mastodonApi.accountUpdateCredentials(
+ displayName, note, locked, avatar, header,
+ field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
+ ).fold(
+ { newProfileData ->
+ saveData.postValue(Success())
+ eventHub.dispatch(ProfileEditedEvent(newProfileData))
+ },
+ { throwable ->
+ if (throwable is HttpException) {
+ val errorResponse = throwable.response()?.errorBody()?.string()
+ val errorMsg = if (!errorResponse.isNullOrBlank()) {
+ try {
+ JSONObject(errorResponse).optString("error", "")
+ } catch (e: JSONException) {
+ null
+ }
+ } else {
null
}
+ saveData.postValue(Error(errorMessage = errorMsg))
} else {
- null
+ saveData.postValue(Error())
}
- saveData.postValue(Error(errorMessage = errorMsg))
- return
}
- saveData.postValue(Success())
- eventHub.dispatch(ProfileEditedEvent(newProfileData))
- }
-
- override fun onFailure(call: Call, t: Throwable) {
- saveData.postValue(Error())
- }
- })
+ )
+ }
}
// cache activity state for rotation change
@@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
return File(application.cacheDir, filename)
}
- override fun onCleared() {
- disposables.dispose()
- }
-
- fun obtainInstance() {
+ fun obtainInstance() = viewModelScope.launch {
if (instanceData.value == null || instanceData.value is Error) {
instanceData.postValue(Loading())
- mastodonApi.getInstance().subscribe(
+ mastodonApi.getInstance().fold(
{ instance ->
instanceData.postValue(Success(instance))
},
@@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
instanceData.postValue(Error())
}
)
- .addTo(disposables)
}
}
}
diff --git a/app/src/main/java/net/accelf/yuito/FooterDrawerItem.kt b/app/src/main/java/net/accelf/yuito/FooterDrawerItem.kt
index 4dfb713a8..5e919cd53 100644
--- a/app/src/main/java/net/accelf/yuito/FooterDrawerItem.kt
+++ b/app/src/main/java/net/accelf/yuito/FooterDrawerItem.kt
@@ -4,7 +4,6 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import autodispose2.SingleSubscribeProxy
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemDrawerFooterBinding
import com.keylesspalace.tusky.entity.Instance
@@ -35,14 +34,13 @@ class FooterDrawerItem : AbstractDrawerItem = throw UnsupportedOperationException()
- fun setSubscribeProxy(subscribeProxy: SingleSubscribeProxy) {
- subscribeProxy.subscribe(
- { instance ->
- binding.instanceData.text = String.format("%s\n%s\n%s", instance.title, instance.uri, instance.version)
- },
- {
- binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
- }
- )
+ fun setInstance(instance: Result) {
+ instance
+ .onSuccess {
+ binding.instanceData.text = String.format("%s\n%s\n%s", it.title, it.uri, it.version)
+ }
+ .onFailure {
+ binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
+ }
}
}
diff --git a/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java b/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java
deleted file mode 100644
index 8b5f076fc..000000000
--- a/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java
+++ /dev/null
@@ -1,150 +0,0 @@
-package net.accelf.yuito;
-
-import android.content.Context;
-import android.text.Spanned;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Px;
-
-import com.google.android.material.button.MaterialButton;
-import com.keylesspalace.tusky.R;
-import com.keylesspalace.tusky.entity.Emoji;
-import com.keylesspalace.tusky.entity.HashTag;
-import com.keylesspalace.tusky.entity.Status;
-import com.keylesspalace.tusky.entity.TimelineAccount;
-import com.keylesspalace.tusky.interfaces.LinkListener;
-import com.keylesspalace.tusky.util.CustomEmojiHelper;
-import com.keylesspalace.tusky.util.ImageLoadingHelper;
-import com.keylesspalace.tusky.util.LinkHelper;
-import com.keylesspalace.tusky.util.StatusDisplayOptions;
-
-import java.util.List;
-
-public class QuoteInlineHelper {
- private final Status quoteStatus;
-
- private final View quoteContainer;
- private final ImageView quoteAvatar;
- private final TextView quoteDisplayName;
- private final TextView quoteUsername;
- private final TextView quoteContentWarningDescription;
- private final MaterialButton quoteContentWarningButton;
- private final TextView quoteContent;
- private final TextView quoteMedia;
-
- private final LinkListener listener;
- @Px
- private final int avatarRadius24dp;
- private final StatusDisplayOptions statusDisplayOptions;
-
- public QuoteInlineHelper(Status status, View container, LinkListener listener,
- @Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
- quoteStatus = status;
- quoteContainer = container;
- quoteAvatar = container.findViewById(R.id.status_quote_inline_avatar);
- quoteDisplayName = container.findViewById(R.id.status_quote_inline_display_name);
- quoteUsername = container.findViewById(R.id.status_quote_inline_username);
- quoteContentWarningDescription = container.findViewById(R.id.status_quote_inline_content_warning_description);
- quoteContentWarningButton = container.findViewById(R.id.status_quote_inline_content_warning_button);
- quoteContent = container.findViewById(R.id.status_quote_inline_content);
- quoteMedia = container.findViewById(R.id.status_quote_inline_media);
- this.listener = listener;
- this.avatarRadius24dp = avatarRadius24dp;
- this.statusDisplayOptions = statusDisplayOptions;
- }
-
- private void setDisplayName(String name, List customEmojis) {
- CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, quoteDisplayName, statusDisplayOptions.animateEmojis());
- quoteDisplayName.setText(emojifiedName);
- }
-
- private void setUsername(String name) {
- Context context = quoteUsername.getContext();
- String format = context.getString(R.string.post_username_format);
- String usernameText = String.format(format, name);
- quoteUsername.setText(usernameText);
- }
-
- private void setContent(
- Spanned content,
- List mentions,
- List tags,
- List emojis,
- LinkListener listener
- ) {
- Spanned singleLineText = SpannedTextHelper.replaceSpanned(content);
- CharSequence emojifiedText = CustomEmojiHelper.emojify(singleLineText, emojis, quoteContent, statusDisplayOptions.animateEmojis());
- LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, tags, listener);
- }
-
- private void setAvatar(String url, @Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
- ImageLoadingHelper.loadAvatar(url, quoteAvatar, avatarRadius24dp, statusDisplayOptions.animateAvatars());
- }
-
- private void setSpoilerText(String spoilerText, List emojis) {
- CharSequence emojiSpoiler =
- CustomEmojiHelper.emojify(spoilerText, emojis, quoteContentWarningDescription, statusDisplayOptions.animateEmojis());
- quoteContentWarningDescription.setText(emojiSpoiler);
- quoteContentWarningDescription.setVisibility(View.VISIBLE);
- quoteContentWarningButton.setVisibility(View.VISIBLE);
- quoteContentWarningButton.setOnClickListener(v
- -> setContentVisibility(!(quoteContent.getVisibility() == View.VISIBLE)));
- setContentVisibility(false);
- }
-
- private void setContentVisibility(boolean show) {
- if (show) {
- quoteContent.setVisibility(View.VISIBLE);
- quoteContentWarningButton.setText(R.string.post_content_warning_show_less);
- } else {
- quoteContent.setVisibility(View.GONE);
- quoteContentWarningButton.setText(R.string.post_content_warning_show_more);
- }
- }
-
- private void hideSpoilerText() {
- quoteContentWarningDescription.setVisibility(View.GONE);
- quoteContentWarningButton.setVisibility(View.GONE);
- quoteContent.setVisibility(View.VISIBLE);
- }
-
- private void setOnClickListener(String accountId, String statusUrl) {
- quoteAvatar.setOnClickListener(view -> listener.onViewAccount(accountId));
- quoteDisplayName.setOnClickListener(view -> listener.onViewAccount(accountId));
- quoteUsername.setOnClickListener(view -> listener.onViewAccount(accountId));
- quoteContent.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
- quoteMedia.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
- quoteContainer.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
- }
-
- public void setupQuoteContainer() {
- TimelineAccount account = quoteStatus.getAccount();
- setDisplayName(account.getName(), account.getEmojis());
- setUsername(account.getUsername());
- setContent(
- quoteStatus.getContent(),
- quoteStatus.getMentions(),
- quoteStatus.getTags(),
- quoteStatus.getEmojis(),
- listener
- );
- setAvatar(account.getAvatar(), avatarRadius24dp, statusDisplayOptions);
- setOnClickListener(account.getId(), quoteStatus.getUrl());
-
- if (quoteStatus.getSpoilerText().isEmpty()) {
- hideSpoilerText();
- } else {
- setSpoilerText(quoteStatus.getSpoilerText(), quoteStatus.getEmojis());
- }
-
- if (quoteStatus.getAttachments().size() == 0) {
- quoteMedia.setVisibility(View.GONE);
- } else {
- quoteMedia.setVisibility(View.VISIBLE);
- quoteMedia.setText(quoteContainer.getContext().getString(R.string.status_quote_media,
- quoteStatus.getAttachments().size()));
- }
- }
-}
diff --git a/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.kt b/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.kt
new file mode 100644
index 000000000..8ec23f28d
--- /dev/null
+++ b/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.kt
@@ -0,0 +1,120 @@
+package net.accelf.yuito
+
+import android.text.Spanned
+import android.view.View
+import androidx.annotation.Px
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.entity.HashTag
+import com.keylesspalace.tusky.entity.Status.Mention
+import com.keylesspalace.tusky.interfaces.LinkListener
+import com.keylesspalace.tusky.util.StatusDisplayOptions
+import com.keylesspalace.tusky.util.emojify
+import com.keylesspalace.tusky.util.loadAvatar
+import com.keylesspalace.tusky.util.setClickableText
+import com.keylesspalace.tusky.viewdata.StatusViewData
+
+class QuoteInlineHelper(
+ private val binding: ViewQuoteInlineBinding,
+ private val listener: LinkListener,
+ @Px private val avatarRadius24dp: Int,
+ private val statusDisplayOptions: StatusDisplayOptions,
+) {
+
+ private fun setDisplayName(name: String, customEmojis: List?) {
+ val viewDisplayName = binding.statusQuoteInlineDisplayName
+ val emojifiedName = name.emojify(customEmojis, viewDisplayName, statusDisplayOptions.animateEmojis)
+ viewDisplayName.text = emojifiedName
+ }
+
+ private fun setUsername(name: String) {
+ val viewUserName = binding.statusQuoteInlineUsername
+ val context = viewUserName.context
+ val format = context.getString(R.string.post_username_format)
+ val usernameText = String.format(format, name)
+ viewUserName.text = usernameText
+ }
+
+ private fun setContent(
+ content: Spanned,
+ mentions: List,
+ tags: List?,
+ emojis: List,
+ ) {
+ val viewContent = binding.statusQuoteInlineContent
+ val singleLineText = SpannedTextHelper.replaceSpanned(content)
+ val emojifiedText = singleLineText.emojify(emojis, viewContent, statusDisplayOptions.animateEmojis)
+ setClickableText(viewContent, emojifiedText, mentions, tags, listener)
+ }
+
+ private fun setAvatar(url: String, @Px avatarRadius24dp: Int, statusDisplayOptions: StatusDisplayOptions) {
+ loadAvatar(url, binding.statusQuoteInlineAvatar, avatarRadius24dp, statusDisplayOptions.animateAvatars)
+ }
+
+ private fun setSpoilerText(spoilerText: String, emojis: List) {
+ val viewDescription = binding.statusQuoteInlineContentWarningDescription
+ val viewButton = binding.statusQuoteInlineContentWarningButton
+ val emojiSpoiler = spoilerText.emojify(emojis, viewDescription, statusDisplayOptions.animateEmojis)
+ viewDescription.text = emojiSpoiler
+ viewDescription.visibility = View.VISIBLE
+ viewButton.visibility = View.VISIBLE
+ viewButton.setOnClickListener {
+ setContentVisibility(binding.statusQuoteInlineContent.visibility != View.VISIBLE)
+ }
+ setContentVisibility(false)
+ }
+
+ private fun setContentVisibility(show: Boolean) {
+ binding.statusQuoteInlineContent.visibility = when (show) {
+ true -> View.VISIBLE
+ false -> View.GONE
+ }
+ binding.statusQuoteInlineContentWarningButton.setText(when (show) {
+ true -> R.string.post_content_warning_show_less
+ false -> R.string.post_content_warning_show_more
+ })
+ }
+
+ private fun hideSpoilerText() {
+ binding.statusQuoteInlineContentWarningDescription.visibility = View.GONE
+ binding.statusQuoteInlineContentWarningButton.visibility = View.GONE
+ binding.statusQuoteInlineContent.visibility = View.VISIBLE
+ }
+
+ private fun setOnClickListener(accountId: String, statusUrl: String?) {
+ binding.statusQuoteInlineAvatar.setOnClickListener { listener.onViewAccount(accountId) }
+ binding.statusQuoteInlineDisplayName.setOnClickListener { listener.onViewAccount(accountId) }
+ binding.statusQuoteInlineUsername.setOnClickListener { listener.onViewAccount(accountId) }
+ binding.statusQuoteInlineContent.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
+ binding.statusQuoteInlineMedia.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
+ binding.root.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
+ }
+
+ fun setupQuoteContainer(quote: StatusViewData.Concrete) {
+ val actionable = quote.actionable
+ val account = actionable.account
+ setDisplayName(account.name, account.emojis)
+ setUsername(account.username)
+ setContent(
+ quote.content,
+ actionable.mentions,
+ actionable.tags,
+ actionable.emojis,
+ )
+ setAvatar(account.avatar, avatarRadius24dp, statusDisplayOptions)
+ setOnClickListener(account.id, actionable.url)
+ if (quote.spoilerText.isEmpty()) {
+ hideSpoilerText()
+ } else {
+ setSpoilerText(quote.spoilerText, actionable.emojis)
+ }
+ val viewMedia = binding.statusQuoteInlineMedia
+ if (actionable.attachments.size == 0) {
+ viewMedia.visibility = View.GONE
+ } else {
+ viewMedia.visibility = View.VISIBLE
+ viewMedia.text = viewMedia.context.getString(R.string.status_quote_media, actionable.attachments.size)
+ }
+ }
+}
diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml
index b55faa6c2..bf2a2d521 100644
--- a/app/src/main/res/values-gd/strings.xml
+++ b/app/src/main/res/values-gd/strings.xml
@@ -253,9 +253,6 @@
- Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
\n(%d caractar(an) air a char as fhaide)
-
-
-
Cha deach leinn am fo-thiotal a shuidheachadh
A’ postadh leis a’ chunntas %1$s
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index e99b718a8..bfabe4c8e 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -54,7 +54,7 @@
Seguir
Tes a certeza de que queres desconectar a conta %1$s\?
Desconectar
- Conecta con Mastodon
+ Accede con Mastodon
Redactar
Máis
Eliminar favorito
@@ -116,7 +116,7 @@
Os ficheiros de vídeo teñen que ser menores de 40MB.
O ficheiro debe ser menor de 8MB.
A publicación é demasiado longa!
- Fallou a obtención do token de conexión.
+ Fallou a obtención do token de acceso.
A autorización foi rexeitada.
Aconteceu un erro non identificado de autorización.
Non se atopou un navegador para utilizar.
@@ -411,8 +411,8 @@
Bloquear @%s\?
Agochar todo o dominio
Tes a certeza de querer bloquear a todo %s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas.
- Eliminar e reescribir este toot\?
- Eliminar este toot\?
+ Eliminar e reescribir esta publicación\?
+ Eliminar esta publicación\?
Deixar de seguir esta conta\?
Revogar a solicitude de seguimento\?
Descargar
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index be5ee7524..0dc453c5c 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -21,23 +21,23 @@
Óskilgreind auðkenningarvilla kom upp.
Heimild var hafnað.
Mistókst að fá innskráningarteikn.
- Stöðufærslan er of löng!
+ Færslan er of löng!
Skráin verður að vera minni en 8MB.
Myndskeiðaskrár verða að vera minni en 40MB.
Þessa tegund skrár er ekki hægt að senda inn.
Ekki var hægt að opna skrána.
Krafist er heimilda til að lesa gögn.
Krafist er heimilda til að geyma gögn.
- Ekki er hægt að hengja bæði myndir og myndskeið við sömu stöðufærslu.
+ Ekki er hægt að hengja bæði myndir og myndskeið við sömu færslu.
Sendingin mistókst.
- Villa við að senda tíst.
+ Villa við að senda færslu.
Heim
Tilkynningar
Staðvært
Sameiginlegt
Bein skilaboð
Flipar
- Tíst
+ Þráður
Færslur
Með svörum
Fest
@@ -62,8 +62,8 @@
Fella saman
Ekkert hér.
Ekkert hér. Togaðu niður til að endurhlaða!
- %s endurbirti tístið þitt
- %s setti tíst frá þér í eftirlæti
+ %s endurbirti færsluna þína
+ %s setti færslu frá þér í eftirlæti
%s fylgist núna með þér
Kæra @%s
Aðrar athugasemdir\?
@@ -117,7 +117,7 @@
Hafna
Drög
Áætluð tíst
- Sýnileiki tísts
+ Sýnileiki færslu
Aðvörun vegna efnis
Lyklaborð með tjáningartáknum
Tímasetja tíst
@@ -234,9 +234,9 @@
Nýir fylgjendur
Tilkynningar um nýja fylgjendur
Endurbirtingar
- Tilkynningar þegar tístin þín eru endurbirt
+ Tilkynningar þegar færslurnar þínar eru endurbirtar
Eftirlæti
- Tilkynningar þegar tístin þín eru sett í eftirlæti
+ Tilkynningar þegar færslurnar þínar eru settar í eftirlæti
Kannanir
Tilkynningar um kannanir sem er lokið
%s minntist á þig
@@ -273,7 +273,7 @@
%ds
Fylgir þér
Alltaf birta myndefni sem merkt er viðkvæmt
- Alltaf fletta út tístum sem eru með aðvörun vegna efnis
+ Alltaf fletta út færslum sem eru með aðvörun vegna efnis
Gagnaskrár
Svar til @%s
hlaða inn fleiru
@@ -376,7 +376,7 @@
Hreinsa
Sía
Virkja
- Semja tíst
+ Semja færslu
Semja skilaboð
Ertu viss um að þú viljir endanlega eyða öllum tilkynningunum þínum\?
Aðgerðir fyrir mynd %s
@@ -478,7 +478,7 @@
Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á:
\n
\n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir
-\n - Eftirlæti/Talningu á endurbirtingum tísta
+\n - Eftirlæti/Talningu á endurbirtingum færslna
\n - Fylgjendur/Tölfræði færslna í notendasniðum
\n
\n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar.
@@ -503,9 +503,9 @@
Takmarka tilkynningar á tímalínu
Yfirfara tilkynningar
Vellíðan
- Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýtt tíst
- Ný tíst
- einhver sem ég er áskrifandi að birti nýtt tíst
+ Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýja færslu
+ Nýjar færslur
+ einhver sem ég er áskrifandi að birti nýja færslu
%s sendi inn rétt í þessu
Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum.
Fjarlægja bókamerki
@@ -518,4 +518,5 @@
180 dagar
365 dagar
14 dagar
+ Semja færslu
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 4c38795d6..04f1a3e8b 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -506,10 +506,10 @@
Ogranicz liczbę powiadomień o zmianach na osi czasu
Czas trwania
Nowe wpisy
- Niektóre informacje, które mogą wpływać na Twoj dobrostan psychiczny zostaną ukryte. W ich skład wchodzą:
+ Niektóre informacje, które mogą wpływać na Twój dobrostan psychiczny zostaną ukryte. W ich skład wchodzą:
\n
\n - powiadomienia o ulubionych/podbiciach/obserwowaniu
-\n - liczba polubień/podbić toota
+\n - liczba polubień/podbić wpisu
\n - statystyki obserwujących/postów na profilach
\n
\nNie będzie to miało wpływu na powiadomienia typu push, ale możesz zmienić ustawienia powiadomień ręcznie.
@@ -542,4 +542,11 @@
%s poprosił(a) o możliwość śledzenia Cię
Usuń z zakładek
Pytaj o potwierdzenie przed dodaniem do ulubionych
+ 14 dni
+ 30 dni
+ 60 dni
+ 90 dni
+ 180 dni
+ 365 dni
+ Utwórz wpis
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 0c78cfa83..0031334b8 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -16,7 +16,7 @@
Сповіщення
Головна
Помилка надсилання допису.
- Зображення та відео не можуть бути прикріплені до статусу одночасно.
+ Зображення та відео не можуть бути прикріплені до допису одночасно.
Потрібен дозвіл на зберігання медіа.
Потрібен дозвіл на читання медіа.
Не вдається відкрити цей файл.
@@ -24,7 +24,7 @@
Аудіофайли повинні бути менше 40 МБ.
Відео повинне бути менше 40 МБ.
Файл повинен бути менше 8 МБ.
- Статус надто довгий!
+ Допис задовгий!
Не вдалося знайти браузер, який можна використати.
Не може бути порожнім.
Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову!
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index cd3f07248..caf759fee 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -2,7 +2,7 @@
Bạn có muốn xóa toàn bộ thông báo\?
Đã lưu tút vào nháp
- Đã hủy đăng
+ Hủy đăng
Đăng Tút
Đang đăng…
@@ -192,7 +192,7 @@
Ghim
Trả lời
Tút
- Chuỗi tút
+ Nội dung tút
Xếp tab
Tin nhắn
Thế giới
@@ -394,7 +394,7 @@
Những người thích tút này
Những người đăng lại tút này
- - %s lượt đăng lại
+ - %s Đăng lại
- %1$s Thích
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9947c3805..a7d2e6e01 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -67,6 +67,7 @@
%s favorited your post
%s followed you
%s requested to follow you
+ %s signed up
%s just posted
Report @%s
@@ -245,6 +246,7 @@
my posts are favorited
polls have ended
somebody I\'m subscribed to published a new post
+ somebody signed up
Appearance
App Theme
Timelines
@@ -321,6 +323,8 @@
Notifications about polls that have ended
New posts
Notifications when somebody you\'re subscribed to published a new post
+ Sign ups
+ Notifications about new users
%s mentioned you
%1$s, %2$s, %3$s and %4$d others
diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
index 19066f42e..8f97f3c42 100644
--- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
@@ -15,16 +15,11 @@
package com.keylesspalace.tusky
-import android.text.SpannedString
-import android.widget.LinearLayout
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.network.MastodonApi
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins
@@ -39,8 +34,8 @@ import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import java.util.ArrayList
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -74,7 +69,7 @@ class BottomSheetActivityTest {
inReplyToId = null,
inReplyToAccountId = null,
reblog = null,
- content = SpannedString("omgwat"),
+ content = "omgwat",
createdAt = Date(),
emojis = emptyList(),
reblogsCount = 0,
@@ -307,7 +302,7 @@ class BottomSheetActivityTest {
init {
mastodonApi = api
@Suppress("UNCHECKED_CAST")
- bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior
+ bottomSheet = mock()
}
override fun openLink(url: String) {
diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
index e7b3a1a91..5396a21ec 100644
--- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
@@ -17,15 +17,12 @@ package com.keylesspalace.tusky
import android.content.Intent
import android.os.Looper.getMainLooper
-import android.text.SpannedString
import android.widget.EditText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
-import com.keylesspalace.tusky.components.compose.MediaUploader
-import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
@@ -37,18 +34,16 @@ import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.InstanceConfiguration
import com.keylesspalace.tusky.entity.StatusConfiguration
import com.keylesspalace.tusky.network.MastodonApi
-import com.keylesspalace.tusky.service.ServiceClient
-import com.nhaarman.mockitokotlin2.any
import io.reactivex.rxjava3.core.Single
-import io.reactivex.rxjava3.core.SingleObserver
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@@ -94,44 +89,47 @@ class ComposeActivityTest {
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
activity = controller.get()
- accountManagerMock = mock(AccountManager::class.java)
- `when`(accountManagerMock.activeAccount).thenReturn(account)
+ accountManagerMock = mock {
+ on { activeAccount } doReturn account
+ }
- apiMock = mock(MastodonApi::class.java)
- `when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList()))
- `when`(apiMock.getInstance()).thenReturn(object : Single() {
- override fun subscribeActual(observer: SingleObserver) {
- val instance = instanceResponseCallback?.invoke()
+ apiMock = mock {
+ on { getCustomEmojis() } doReturn Single.just(emptyList())
+ onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
if (instance == null) {
- observer.onError(Throwable())
+ Result.failure(Throwable())
} else {
- observer.onSuccess(instance)
+ Result.success(instance)
}
}
- })
+ }
- val instanceDaoMock = mock(InstanceDao::class.java)
- `when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn(
- Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
- )
+ val instanceDaoMock: InstanceDao = mock {
+ on { loadMetadataForInstance(any()) } doReturn
+ Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
+ on { loadMetadataForInstance(any()) } doReturn
+ Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
+ }
- val dbMock = mock(AppDatabase::class.java)
- `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
+ val dbMock: AppDatabase = mock {
+ on { instanceDao() } doReturn instanceDaoMock
+ }
val viewModel = ComposeViewModel(
apiMock,
accountManagerMock,
- mock(MediaUploader::class.java),
- mock(ServiceClient::class.java),
- mock(DraftHelper::class.java),
+ mock(),
+ mock(),
+ mock(),
dbMock
)
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
}
- val viewModelFactoryMock = mock(ViewModelFactory::class.java)
- `when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel)
+ val viewModelFactoryMock: ViewModelFactory = mock {
+ on { create(ComposeViewModel::class.java) } doReturn viewModel
+ }
activity.accountManager = accountManagerMock
activity.viewModelFactory = viewModelFactoryMock
@@ -470,7 +468,7 @@ class ComposeActivityTest {
"admin",
"admin",
"admin",
- SpannedString(""),
+ "",
"https://example.token",
"",
"",
@@ -490,7 +488,7 @@ class ComposeActivityTest {
)
}
- fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
+ private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
return InstanceConfiguration(
statuses = StatusConfiguration(
maxCharacters = maximumStatusCharacters,
diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
index fd8994e09..d3455ec27 100644
--- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
@@ -1,6 +1,5 @@
package com.keylesspalace.tusky
-import android.text.SpannedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Filter
@@ -8,12 +7,12 @@ import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel
-import com.nhaarman.mockitokotlin2.mock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
import java.util.ArrayList
import java.util.Date
@@ -22,7 +21,7 @@ import java.util.Date
@RunWith(AndroidJUnit4::class)
class FilterTest {
- lateinit var filterModel: FilterModel
+ private lateinit var filterModel: FilterModel
@Before
fun setup() {
@@ -162,7 +161,7 @@ class FilterTest {
inReplyToId = null,
inReplyToAccountId = null,
reblog = null,
- content = SpannedString(content),
+ content = content,
createdAt = Date(),
emojis = emptyList(),
reblogsCount = 0,
diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt
index ed06e27c6..3086036a0 100644
--- a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt
@@ -1,10 +1,8 @@
package com.keylesspalace.tusky
-import android.text.Spanned
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.json.SpannedTypeAdapter
import com.keylesspalace.tusky.viewdata.StatusViewData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@@ -39,9 +37,7 @@ class StatusComparisonTest {
assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
}
- private val gson = GsonBuilder().registerTypeAdapter(
- Spanned::class.java, SpannedTypeAdapter()
- ).create()
+ private val gson = Gson()
@Test
fun `two equal status view data - should be equal`() {
@@ -49,14 +45,12 @@ class StatusComparisonTest {
status = createStatus(),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
assertEquals(viewdata1, viewdata2)
@@ -68,14 +62,12 @@ class StatusComparisonTest {
status = createStatus(),
isExpanded = true,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
assertNotEquals(viewdata1, viewdata2)
@@ -87,14 +79,12 @@ class StatusComparisonTest {
status = createStatus(content = "whatever"),
isExpanded = true,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = false
)
assertNotEquals(viewdata1, viewdata2)
diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt
index 462b0a4a0..2778f8c26 100644
--- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt
@@ -17,9 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
-import com.nhaarman.mockitokotlin2.anyOrNull
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import retrofit2.HttpException
diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt
index 2e67c6fe3..33215e675 100644
--- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt
@@ -1,14 +1,19 @@
package com.keylesspalace.tusky.components.timeline
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.NetworkTimelineViewModel
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.robolectric.annotation.Config
+@Config(sdk = [28])
+@RunWith(AndroidJUnit4::class)
class NetworkTimelinePagingSourceTest {
private val status = mockStatusViewData()
diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt
index 74d0fe257..eabf744c2 100644
--- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt
@@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.viewdata.StatusViewData
-import com.nhaarman.mockitokotlin2.anyOrNull
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.doThrow
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.runBlocking
import okhttp3.Headers
import okhttp3.ResponseBody.Companion.toResponseBody
@@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doThrow
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest {
mockStatusViewData("2"),
mockStatusViewData("1"),
)
-
verify(timelineViewModel).nextKey = "0"
assertTrue(result is RemoteMediator.MediatorResult.Success)
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
index 13a7b338a..d4ca1f4de 100644
--- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
@@ -1,6 +1,5 @@
package com.keylesspalace.tusky.components.timeline
-import android.text.SpannedString
import com.google.gson.Gson
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.keylesspalace.tusky.entity.Status
@@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status(
inReplyToId = null,
inReplyToAccountId = null,
reblog = null,
- content = SpannedString("Test"),
+ content = "Test",
createdAt = fixedDate,
emojis = emptyList(),
reblogsCount = 1,
@@ -51,7 +50,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete(
status = mockStatus(id),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = true,
)
diff --git a/build.gradle b/build.gradle
index c93117011..3a5251fa0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,4 @@
buildscript {
- ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
@@ -7,12 +6,12 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:7.1.2"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20"
+ classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1"
}
}
plugins {
- id "org.jlleitschuh.gradle.ktlint" version "10.1.0"
+ id "org.jlleitschuh.gradle.ktlint" version "10.2.1"
}
allprojects {
diff --git a/fastlane/metadata/android/pl/changelogs/87.txt b/fastlane/metadata/android/pl/changelogs/87.txt
new file mode 100644
index 000000000..2b8d41f69
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky v16.0
+
+- Logika ładowania osi czasu została przepisana w celu przyspieszenia jej i naprawienia błędów.
+- Tusky wspiera teraz animowane emotikony w formatach APNG i Animated WebP.
+- Mnóstwo poprawek
+- Wsparcie dla Androida 11
+- Nowe tłumaczenia: Gaelicki szkocki, galicyjski, ukraiński
+- Ulepszone tłumaczenia
diff --git a/fastlane/metadata/android/uk/changelogs/89.txt b/fastlane/metadata/android/uk/changelogs/89.txt
new file mode 100644
index 000000000..fbed5e83d
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- «Відкрити як...» тепер також доступно в меню профілів облікових записів за користування кількома обліковими записами
+- Тепер вхід обробляється у WebView у застосунку
+- Підтримка Android 12
+- підтримка нового API конфігурації сервера Mastodon
+- і багато інших дрібних виправлень і вдосконалень