diff --git a/app/build.gradle b/app/build.gradle
index b153ae0b..19e094d5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,9 +7,13 @@ apply from: "../instance-build.gradle"
def getGitSha = {
def stdout = new ByteArrayOutputStream()
- exec {
- commandLine 'git', 'rev-parse', '--short', 'HEAD'
- standardOutput = stdout
+ try {
+ exec {
+ commandLine 'git', 'rev-parse', '--short', 'HEAD'
+ standardOutput = stdout
+ }
+ } catch (Exception e) {
+ return "unknown"
}
return stdout.toString().trim()
}
@@ -21,7 +25,7 @@ android {
minSdkVersion 21
targetSdkVersion 31
versionCode 87
- versionName "17.0-CW1"
+ versionName "18.0-CW1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -89,7 +93,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'
@@ -97,11 +101,11 @@ ext.okhttpVersion = '4.9.3'
ext.glideVersion = '4.13.1'
ext.daggerVersion = '2.41'
ext.materialdrawerVersion = '8.4.5'
+ext.emoji2_version = '1.1.0'
+ext.filemojicompat_version = '3.2.1'
// 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"
@@ -115,8 +119,9 @@ dependencies {
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
- implementation "androidx.emoji:emoji:1.1.0"
- implementation "androidx.emoji:emoji-appcompat:1.1.0"
+ implementation "androidx.emoji2:emoji2:$emoji2_version"
+ implementation "androidx.emoji2:emoji2-views:$emoji2_version"
+ implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
@@ -127,7 +132,6 @@ dependencies {
implementation "androidx.work:work-runtime:2.7.1"
implementation "androidx.room:room-ktx:$roomVersion"
implementation "androidx.room:room-paging:$roomVersion"
- implementation "androidx.room:room-rxjava3:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
@@ -138,6 +142,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"
@@ -173,12 +178,14 @@ dependencies {
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
- implementation "de.c1710:filemojicompat:1.0.18"
+ implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
+ implementation "de.c1710:filemojicompat:$filemojicompat_version"
+ implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version"
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 00000000..97ad414e
--- /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 00000000..e6d8ec7d
--- /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/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json
new file mode 100644
index 00000000..c1354690
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json
@@ -0,0 +1,815 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 34,
+ "identityHash": "7f766d68ab5d72a7988cd81c183e9a9d",
+ "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, `notificationsUpdates` 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": "notificationsUpdates",
+ "columnName": "notificationsUpdates",
+ "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, '7f766d68ab5d72a7988cd81c183e9a9d')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json
new file mode 100644
index 00000000..9b71adf2
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json
@@ -0,0 +1,821 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 35,
+ "identityHash": "9e6c0bb60538683a16c30fa3e1cc24f2",
+ "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, `notificationsUpdates` 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": "notificationsUpdates",
+ "columnName": "notificationsUpdates",
+ "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, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "serverId",
+ "columnName": "serverId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timelineUserId",
+ "columnName": "timelineUserId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authorServerId",
+ "columnName": "authorServerId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToId",
+ "columnName": "inReplyToId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToAccountId",
+ "columnName": "inReplyToAccountId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "emojis",
+ "columnName": "emojis",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "reblogsCount",
+ "columnName": "reblogsCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favouritesCount",
+ "columnName": "favouritesCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "reblogged",
+ "columnName": "reblogged",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookmarked",
+ "columnName": "bookmarked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favourited",
+ "columnName": "favourited",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sensitive",
+ "columnName": "sensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "spoilerText",
+ "columnName": "spoilerText",
+ "affinity": "TEXT",
+ "notNull": 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
+ },
+ {
+ "fieldPath": "card",
+ "columnName": "card",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "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, '9e6c0bb60538683a16c30fa3e1cc24f2')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 5c7901c5..25b70240 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -35,8 +35,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.emoji.text.EmojiCompat
-import androidx.emoji.text.EmojiCompat.InitCallback
+import androidx.core.view.GravityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@@ -114,6 +113,7 @@ import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
+import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch
@@ -150,13 +150,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private var accountLocked: Boolean = false
- private val emojiInitCallback = object : InitCallback() {
- override fun onInitialized() {
- if (!isDestroyed) {
- updateProfiles()
- }
- }
- }
+ // We need to know if the emoji pack has been changed
+ private var selectedEmojiPack: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -271,11 +266,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
}
+
+ selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
}
override fun onResume() {
super.onResume()
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager)
+ val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
+ if (currentEmojiPack != selectedEmojiPack) {
+ Log.d(
+ TAG,
+ "onResume: EmojiPack has been changed from %s to %s"
+ .format(selectedEmojiPack, currentEmojiPack)
+ )
+ selectedEmojiPack = currentEmojiPack
+ recreate()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // For some reason the navigation drawer is opened when the activity is recreated
+ if (binding.mainDrawerLayout.isOpen) {
+ binding.mainDrawerLayout.closeDrawer(GravityCompat.START, false)
+ }
}
override fun onBackPressed() {
@@ -333,11 +348,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
- override fun onDestroy() {
- super.onDestroy()
- EmojiCompat.get().unregisterInitCallback(emojiInitCallback)
- }
-
private fun forwardShare(intent: Intent) {
val composeIntent = Intent(this, ComposeActivity::class.java)
composeIntent.action = intent.action
@@ -530,7 +540,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}
- EmojiCompat.get().registerInitCallback(emojiInitCallback)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -612,6 +621,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
binding.mainToolbar.setOnClickListener {
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
}
+
+ updateProfiles()
}
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
@@ -682,18 +693,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
- private fun fetchUserInfo() {
- mastodonApi.accountVerifyCredentials()
- .observeOn(AndroidSchedulers.mainThread())
- .autoDispose(this, Lifecycle.Event.ON_DESTROY)
- .subscribe(
- { userInfo ->
- 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) {
@@ -782,18 +790,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun fetchAnnouncements() {
- mastodonApi.listAnnouncements(false)
- .observeOn(AndroidSchedulers.mainThread())
- .autoDispose(this, Lifecycle.Event.ON_DESTROY)
- .subscribe(
- { announcements ->
- unreadAnnouncementsCount = announcements.count { !it.read }
- updateAnnouncementsBadge()
- },
- {
- Log.w(TAG, "Failed to fetch announcements.", it)
- }
- )
+ lifecycleScope.launch {
+ mastodonApi.listAnnouncements(false)
+ .fold(
+ { announcements ->
+ unreadAnnouncementsCount = announcements.count { !it.read }
+ updateAnnouncementsBadge()
+ },
+ { throwable ->
+ Log.w(TAG, "Failed to fetch announcements.", throwable)
+ }
+ )
+ }
}
private fun updateAnnouncementsBadge() {
@@ -803,11 +811,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc ->
- val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
-
ProfileDrawerItem().apply {
isSelected = acc.isActive
- nameText = emojifiedName
+ nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis)
iconUrl = acc.profilePictureUrl
isNameShown = true
identifier = acc.id
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
index 0339a7bc..ded947a8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -19,18 +19,18 @@ import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
-import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
-import com.keylesspalace.tusky.settings.PrefKeys
-import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.ThemeUtils
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
+import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
+import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
+import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt
import java.security.Security
@@ -65,12 +65,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
- // init the custom emoji fonts
- val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
- val emojiConfig = EmojiCompatFont.byId(emojiSelection)
- .getConfig(this)
- .setReplaceAll(true)
- EmojiCompat.init(emojiConfig)
+ // In this case, we want to have the emoji preferences merged with the other ones
+ // Copied from PreferenceManager.getDefaultSharedPreferenceName
+ EmojiPreference.sharedPreferenceName = packageName + "_preferences"
+ EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
// init night mode
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
index 64d29577..fda2c82b 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/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java
index f4824389..6672fff3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java
@@ -45,8 +45,7 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
if (showBotOverlay && account.getBot()) {
avatarInset.setVisibility(View.VISIBLE);
- avatarInset.setImageResource(R.drawable.ic_bot_24dp);
- avatarInset.setBackgroundColor(0x50ffffff);
+ avatarInset.setImageResource(R.drawable.bot_badge);
} else {
avatarInset.setVisibility(View.GONE);
}
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 8f022909..c481bbf5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
@@ -32,6 +32,8 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
@@ -47,6 +49,7 @@ import com.keylesspalace.tusky.entity.TimelineAccount;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
+import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
@@ -58,10 +61,8 @@ import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
-import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
-import java.util.Locale;
import at.connyduck.sparkbutton.helpers.Utils;
@@ -90,6 +91,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
private AdapterDataSource dataSource;
+ private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
public NotificationsAdapter(String accountId,
AdapterDataSource dataSource,
@@ -119,7 +121,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS_NOTIFICATION: {
View view = inflater
.inflate(R.layout.item_status_notification, parent, false);
- return new StatusNotificationViewHolder(view, statusDisplayOptions);
+ return new StatusNotificationViewHolder(view, statusDisplayOptions, absoluteTimeFormatter);
}
case VIEW_TYPE_FOLLOW: {
View view = inflater
@@ -178,8 +180,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS: {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
- holder.setupWithStatus(status,
- statusListener, statusDisplayOptions, payloadForHolder);
+ if (status == null) {
+ /* in some very rare cases servers sends null status even though they should not,
+ * we have to handle it somehow */
+ holder.showStatusContent(false);
+ } else {
+ if (payloads == null) {
+ holder.showStatusContent(true);
+ }
+ holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
+ }
if (concreteNotificaton.getType() == Notification.Type.POLL) {
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
} else {
@@ -192,6 +202,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
if (payloadForHolder == null) {
if (statusViewData == null) {
+ /* in some very rare cases servers sends null status even though they should not,
+ * we have to handle it somehow */
holder.showNotificationContent(false);
} else {
holder.showNotificationContent(true);
@@ -201,7 +213,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
holder.setUsername(status.getAccount().getUsername());
holder.setCreatedAt(status.getCreatedAt());
- if (concreteNotificaton.getType() == Notification.Type.STATUS) {
+ if (concreteNotificaton.getType() == Notification.Type.STATUS ||
+ concreteNotificaton.getType() == Notification.Type.UPDATE) {
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
} else {
holder.setAvatars(status.getAccount().getAvatar(),
@@ -226,7 +239,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;
@@ -280,10 +293,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
case STATUS:
case FAVOURITE:
- case REBLOG: {
+ case REBLOG:
+ case UPDATE: {
return VIEW_TYPE_STATUS_NOTIFICATION;
}
- case FOLLOW: {
+ case FOLLOW:
+ case SIGN_UP: {
return VIEW_TYPE_FOLLOW;
}
case FOLLOW_REQUEST: {
@@ -335,10 +350,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(
@@ -382,19 +397,22 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private final Button contentWarningButton;
private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
private StatusDisplayOptions statusDisplayOptions;
+ private final AbsoluteTimeFormatter absoluteTimeFormatter;
private String accountId;
private String notificationId;
private NotificationActionListener notificationActionListener;
private StatusViewData.Concrete statusViewData;
- private SimpleDateFormat shortSdf;
- private SimpleDateFormat longSdf;
private int avatarRadius48dp;
private int avatarRadius36dp;
private int avatarRadius24dp;
- StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
+ StatusNotificationViewHolder(
+ View itemView,
+ StatusDisplayOptions statusDisplayOptions,
+ AbsoluteTimeFormatter absoluteTimeFormatter
+ ) {
super(itemView);
message = itemView.findViewById(R.id.notification_top_text);
statusNameBar = itemView.findViewById(R.id.status_name_bar);
@@ -408,6 +426,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
this.statusDisplayOptions = statusDisplayOptions;
+ this.absoluteTimeFormatter = absoluteTimeFormatter;
int darkerFilter = Color.rgb(123, 123, 123);
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
@@ -416,8 +435,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
itemView.setOnClickListener(this);
message.setOnClickListener(this);
statusContent.setOnClickListener(this);
- shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
- longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
@@ -447,17 +464,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
protected void setCreatedAt(@Nullable Date createdAt) {
if (statusDisplayOptions.useAbsoluteTime()) {
- String time;
- if (createdAt != null) {
- if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
- time = longSdf.format(createdAt);
- } else {
- time = shortSdf.format(createdAt);
- }
- } else {
- time = "??:??:??";
- }
- timestampInfo.setText(time);
+ timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
} else {
// This is the visible timestampInfo.
String readout;
@@ -481,6 +488,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
}
+ Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) {
+ Drawable icon = ContextCompat.getDrawable(context, drawable);
+ if (icon != null) {
+ icon.setColorFilter(ContextCompat.getColor(context, color), PorterDuff.Mode.SRC_ATOP);
+ }
+ return icon;
+ }
+
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
this.statusViewData = notificationViewData.getStatusViewData();
@@ -493,41 +508,36 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
switch (type) {
default:
case FAVOURITE: {
- icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp);
- if (icon != null) {
- icon.setColorFilter(ContextCompat.getColor(context,
- R.color.tusky_orange), PorterDuff.Mode.SRC_ATOP);
- }
-
+ icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange);
format = context.getString(R.string.notification_favourite_format);
break;
}
case REBLOG: {
- icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp);
- if (icon != null) {
- icon.setColorFilter(ContextCompat.getColor(context,
- R.color.chinwag_green), PorterDuff.Mode.SRC_ATOP);
- }
-
+ icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.chinwag_green);
format = context.getString(R.string.notification_reblog_format);
break;
}
case STATUS: {
- icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp);
- if (icon != null) {
- icon.setColorFilter(ContextCompat.getColor(context,
- R.color.chinwag_green), PorterDuff.Mode.SRC_ATOP);
- }
-
+ icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.chinwag_green);
format = context.getString(R.string.notification_subscription_format);
break;
}
+ case UPDATE: {
+ icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.chinwag_green);
+ format = context.getString(R.string.notification_update_format);
+ break;
+ }
}
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
String wholeMessage = String.format(format, displayName);
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
- str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ int displayNameIndex = format.indexOf("%s");
+ str.setSpan(
+ new StyleSpan(Typeface.BOLD),
+ displayNameIndex,
+ displayNameIndex + displayName.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
CharSequence emojifiedText = CustomEmojiHelper.emojify(
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
);
@@ -570,9 +580,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
if (statusDisplayOptions.showBotOverlay() && isBot) {
notificationAvatar.setVisibility(View.VISIBLE);
- notificationAvatar.setBackgroundColor(0x50ffffff);
Glide.with(notificationAvatar)
- .load(R.drawable.ic_bot_24dp)
+ .load(ContextCompat.getDrawable(notificationAvatar.getContext(), R.drawable.bot_badge))
.into(notificationAvatar);
} else {
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
index 1a60d860..ef366795 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
@@ -19,7 +19,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
-import androidx.emoji.text.EmojiCompat
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemPollBinding
@@ -87,9 +86,8 @@ class PollAdapter : RecyclerView.Adapter>() {
when (mode) {
RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
- val emojifiedPollOptionText = buildDescription(option.title, percent, option.voted, resultTextView.context)
+ resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context)
.emojify(emojis, resultTextView, animateEmojis)
- resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100
val optionColor = if (option.voted) {
@@ -103,8 +101,7 @@ class PollAdapter : RecyclerView.Adapter>() {
resultTextView.setOnClickListener(resultClickListener)
}
SINGLE -> {
- val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis)
- radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
+ radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis)
radioButton.isChecked = option.selected
radioButton.setOnClickListener {
pollOptions.forEachIndexed { index, pollOption ->
@@ -114,8 +111,7 @@ class PollAdapter : RecyclerView.Adapter>() {
}
}
MULTIPLE -> {
- val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis)
- checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
+ checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis)
checkBox.isChecked = option.selected
checkBox.setOnCheckedChangeListener { _, isChecked ->
pollOptions[holder.bindingAdapterPosition].selected = isChecked
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 1239ea71..2a5b3f2c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
@@ -20,6 +20,8 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -27,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.google.android.material.button.MaterialButton;
@@ -40,6 +43,7 @@ import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.HashTag;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
+import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
@@ -54,10 +58,8 @@ import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.NumberFormat;
-import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
-import java.util.Locale;
import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.helpers.Utils;
@@ -77,6 +79,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private SparkButton favouriteButton;
private SparkButton bookmarkButton;
private ImageButton moreButton;
+ private ConstraintLayout mediaContainer;
protected MediaPreviewImageView[] mediaPreviews;
private ImageView[] mediaOverlays;
private TextView sensitiveMediaWarning;
@@ -103,10 +106,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private TextView cardUrl;
private PollAdapter pollAdapter;
- private SimpleDateFormat shortSdf;
- private SimpleDateFormat longSdf;
-
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
+ private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
protected int avatarRadius48dp;
private int avatarRadius36dp;
@@ -127,7 +128,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
moreButton = itemView.findViewById(R.id.status_more);
- itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true);
+ mediaContainer = itemView.findViewById(R.id.status_media_preview_container);
+ mediaContainer.setClipToOutline(true);
mediaPreviews = new MediaPreviewImageView[]{
itemView.findViewById(R.id.status_media_preview_0),
@@ -170,9 +172,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
- this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
- this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
-
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
@@ -290,11 +289,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (statusDisplayOptions.showBotOverlay() && isBot) {
avatarInset.setVisibility(View.VISIBLE);
- avatarInset.setBackgroundColor(0x50ffffff);
Glide.with(avatarInset)
- .load(R.drawable.ic_bot_24dp)
+ // passing the drawable id directly into .load() ignores night mode https://github.com/bumptech/glide/issues/4692
+ .load(ContextCompat.getDrawable(avatarInset.getContext(), R.drawable.bot_badge))
.into(avatarInset);
-
} else {
avatarInset.setVisibility(View.GONE);
}
@@ -320,7 +318,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime()) {
- timestampInfo.setText(getAbsoluteTime(createdAt));
+ timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
} else {
if (createdAt == null) {
timestampInfo.setText("?m");
@@ -333,21 +331,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
- private String getAbsoluteTime(Date createdAt) {
- if (createdAt == null) {
- return "??:??:??";
- }
- if (DateUtils.isToday(createdAt.getTime())) {
- return shortSdf.format(createdAt);
- } else {
- return longSdf.format(createdAt);
- }
- }
-
private CharSequence getCreatedAtDescription(Date createdAt,
StatusDisplayOptions statusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime()) {
- return getAbsoluteTime(createdAt);
+ return absoluteTimeFormatter.format(createdAt, true);
} else {
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
* as 17 meters instead of minutes. */
@@ -736,9 +723,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.setupWithStatus(status, listener, statusDisplayOptions, null);
}
- public void setupWithStatus(StatusViewData.Concrete status,
- final StatusActionListener listener,
- StatusDisplayOptions statusDisplayOptions,
+ public void setupWithStatus(@NonNull StatusViewData.Concrete status,
+ @NonNull final StatusActionListener listener,
+ @NonNull StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
if (payloads == null) {
Status actionable = status.getActionable();
@@ -1028,7 +1015,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return votesText;
} else {
if (statusDisplayOptions.useAbsoluteTime()) {
- pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
+ pollDurationInfo = context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.getExpiresAt(), false));
} else {
pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
}
@@ -1043,9 +1030,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())) {
@@ -1067,7 +1056,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;
@@ -1148,6 +1137,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
+ public void showStatusContent(boolean show) {
+ int visibility = show ? View.VISIBLE : View.GONE;
+ avatar.setVisibility(visibility);
+ avatarInset.setVisibility(visibility);
+ displayName.setVisibility(visibility);
+ username.setVisibility(visibility);
+ timestampInfo.setVisibility(visibility);
+ contentWarningDescription.setVisibility(visibility);
+ contentWarningButton.setVisibility(visibility);
+ content.setVisibility(visibility);
+ cardView.setVisibility(visibility);
+ mediaContainer.setVisibility(visibility);
+ pollOptions.setVisibility(visibility);
+ pollButton.setVisibility(visibility);
+ pollDescription.setVisibility(visibility);
+ replyButton.setVisibility(visibility);
+ reblogButton.setVisibility(visibility);
+ favouriteButton.setVisibility(visibility);
+ bookmarkButton.setVisibility(visibility);
+ moreButton.setVisibility(visibility);
+ }
+
private static String formatDuration(double durationInSeconds) {
int seconds = (int) Math.round(durationInSeconds) % 60;
int minutes = (int) durationInSeconds % 3600 / 60;
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
index 56adfcad..bf2c05e0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
@@ -1,14 +1,12 @@
package com.keylesspalace.tusky.adapter;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
-import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
@@ -101,10 +99,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
- public void setupWithStatus(final StatusViewData.Concrete status,
- final StatusActionListener listener,
- StatusDisplayOptions statusDisplayOptions,
- @Nullable Object payloads) {
+ public void setupWithStatus(@NonNull final StatusViewData.Concrete status,
+ @NonNull final StatusActionListener listener,
+ @NonNull StatusDisplayOptions statusDisplayOptions,
+ @Nullable Object payloads) {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
if (payloads == null) {
@@ -118,19 +116,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
setApplication(status.getActionable().getApplication());
- View.OnLongClickListener longClickListener = view -> {
- TextView textView = (TextView) view;
- ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("toot", textView.getText());
- clipboard.setPrimaryClip(clip);
-
- Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show();
-
- return true;
- };
-
- content.setOnLongClickListener(longClickListener);
- contentWarningDescription.setOnLongClickListener(longClickListener);
setStatusVisibility(status.getActionable().getVisibility());
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
index b054aea9..93c47564 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
@@ -22,6 +22,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
@@ -58,9 +59,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
}
@Override
- public void setupWithStatus(StatusViewData.Concrete status,
- final StatusActionListener listener,
- StatusDisplayOptions statusDisplayOptions,
+ public void setupWithStatus(@NonNull StatusViewData.Concrete status,
+ @NonNull final StatusActionListener listener,
+ @NonNull StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
if (payloads == null) {
@@ -129,4 +130,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
content.setFilters(NO_INPUT_FILTER);
}
}
+
+ public void showStatusContent(boolean show) {
+ super.showStatusContent(show);
+ contentCollapseButton.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
}
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 d02c9cce..8f53645d 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
@@ -20,8 +20,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.text.Editable
import android.view.Menu
@@ -39,7 +37,6 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
-import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer
@@ -78,7 +75,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
@@ -374,13 +371,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.show()
}
}
- viewModel.accountFieldData.observe(
- this,
- {
- accountFieldAdapter.fields = it
- accountFieldAdapter.notifyDataSetChanged()
- }
- )
viewModel.noteSaved.observe(this) {
binding.saveNoteInfo.visible(it, View.INVISIBLE)
}
@@ -395,11 +385,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.chinwag_green)
}
@@ -410,10 +399,10 @@ 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()
+ accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged()
@@ -469,14 +458,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
*/
private fun updateToolbar() {
loadedAccount?.let { account ->
-
- val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
-
- try {
- supportActionBar?.title = EmojiCompat.get().process(emojifiedName)
- } catch (e: IllegalStateException) {
- supportActionBar?.title = emojifiedName
- }
+ supportActionBar?.title = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username)
}
}
@@ -501,13 +483,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar)
binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name)
-
- // this is necessary because API 19 can't handle vector compound drawables
- val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
- val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
- movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
-
- binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
}
}
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 093dbcfb..86acb813 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
@@ -15,7 +15,6 @@
package com.keylesspalace.tusky.components.account
-import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@@ -23,12 +22,10 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field
-import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener
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(
@@ -37,7 +34,7 @@ class AccountFieldAdapter(
) : RecyclerView.Adapter>() {
var emojis: List = emptyList()
- var fields: List> = emptyList()
+ var fields: List = emptyList()
override fun getItemCount() = fields.size
@@ -47,32 +44,20 @@ class AccountFieldAdapter(
}
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
- val proofOrField = fields[position]
+ val field = fields[position]
val nameTextView = holder.binding.accountFieldName
val valueTextView = holder.binding.accountFieldValue
- if (proofOrField.isLeft()) {
- val identityProof = proofOrField.asLeft()
+ val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
+ nameTextView.text = emojifiedName
- nameTextView.text = identityProof.provider
- valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
-
- valueTextView.movementMethod = LinkMovementMethod.getInstance()
+ val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
+ setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
+ if (field.verifiedAt != null) {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else {
- val field = proofOrField.asRight()
- val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
- nameTextView.text = emojifiedName
-
- val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
- setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
-
- if (field.verifiedAt != null) {
- valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
- } else {
- valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
- }
+ valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt
index 6fa988ac..664651eb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt
@@ -10,17 +10,13 @@ import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.appstore.UnfollowEvent
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Account
-import com.keylesspalace.tusky.entity.Field
-import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.network.MastodonApi
-import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
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.combineOptionalLiveData
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable
import retrofit2.Call
@@ -40,13 +36,6 @@ class AccountViewModel @Inject constructor(
val noteSaved = MutableLiveData()
- private val identityProofData = MutableLiveData>()
-
- val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
- identityProofs.orEmpty().map { Either.Left(it) }
- .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
- }
-
val isRefreshing = MutableLiveData().apply { value = false }
private var isDataLoading = false
@@ -106,22 +95,6 @@ class AccountViewModel @Inject constructor(
}
}
- private fun obtainIdentityProof(reload: Boolean = false) {
- if (identityProofData.value == null || reload) {
-
- mastodonApi.identityProofs(accountId)
- .subscribe(
- { proofs ->
- identityProofData.postValue(proofs)
- },
- { t ->
- Log.w(TAG, "failed obtaining identity proofs", t)
- }
- )
- .autoDispose()
- }
- }
-
fun changeFollowState() {
val relationship = relationshipData.value?.data
if (relationship?.following == true || relationship?.requested == true) {
@@ -314,7 +287,6 @@ class AccountViewModel @Inject constructor(
return
accountId.let {
obtainAccount(isReload)
- obtainIdentityProof()
if (!isSelf)
obtainRelationship(isReload)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
index 4b5e7aa5..70ebfc7d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
@@ -32,6 +32,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.EmojiSpan
import com.keylesspalace.tusky.util.emojify
+import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.setClickableText
import java.lang.ref.WeakReference
@@ -60,7 +61,7 @@ class AnnouncementAdapter(
val chips = holder.binding.chipGroup
val addReactionChip = holder.binding.addReactionChip
- val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
+ val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(item.emojis, text, animateEmojis)
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
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 d1ae0b9e..0934c48f 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
@@ -18,31 +18,26 @@ package com.keylesspalace.tusky.components.announcements
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.EventHub
-import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
-import com.keylesspalace.tusky.db.AccountManager
-import com.keylesspalace.tusky.db.AppDatabase
-import com.keylesspalace.tusky.db.InstanceEntity
+import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.Emoji
-import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi
-import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
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 io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.launch
import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor(
- accountManager: AccountManager,
- private val appDatabase: AppDatabase,
+ private val instanceInfoRepo: InstanceInfoRepository,
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
-) : RxAwareViewModel() {
+) : ViewModel() {
private val announcementsMutable = MutableLiveData>>()
val announcements: LiveData>> = announcementsMutable
@@ -51,155 +46,130 @@ class AnnouncementsViewModel @Inject constructor(
val emojis: LiveData> = emojisMutable
init {
- Single.zip(
- mastodonApi.getCustomEmojis(),
- appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
- .map> { Either.Left(it) }
- .onErrorResumeNext {
- mastodonApi.getInstance()
- .map { Either.Right(it) }
- }
- ) { emojis, either ->
- either.asLeftOrNull()?.copy(emojiList = emojis)
- ?: InstanceEntity(
- accountManager.activeAccount?.domain!!,
- emojis,
- either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars,
- either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions,
- either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars,
- either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration,
- either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration,
- either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
- either.asRight().version
- )
+ viewModelScope.launch {
+ emojisMutable.postValue(instanceInfoRepo.getEmojis())
}
- .doOnSuccess {
- appDatabase.instanceDao().insertOrReplace(it)
- }
- .subscribe(
- {
- emojisMutable.postValue(it.emojiList.orEmpty())
- },
- {
- Log.w(TAG, "Failed to get custom emojis.", it)
- }
- )
- .autoDispose()
}
fun load() {
- announcementsMutable.postValue(Loading())
- mastodonApi.listAnnouncements()
- .subscribe(
- {
- announcementsMutable.postValue(Success(it))
- it.filter { announcement -> !announcement.read }
- .forEach { announcement ->
- mastodonApi.dismissAnnouncement(announcement.id)
- .subscribe(
- {
- eventHub.dispatch(AnnouncementReadEvent(announcement.id))
- },
- { throwable ->
- Log.d(TAG, "Failed to mark announcement as read.", throwable)
- }
- )
- .autoDispose()
- }
- },
- {
- announcementsMutable.postValue(Error(cause = it))
- }
- )
- .autoDispose()
+ viewModelScope.launch {
+ announcementsMutable.postValue(Loading())
+ mastodonApi.listAnnouncements()
+ .fold(
+ {
+ announcementsMutable.postValue(Success(it))
+ it.filter { announcement -> !announcement.read }
+ .forEach { announcement ->
+ mastodonApi.dismissAnnouncement(announcement.id)
+ .fold(
+ {
+ eventHub.dispatch(AnnouncementReadEvent(announcement.id))
+ },
+ { throwable ->
+ Log.d(
+ TAG,
+ "Failed to mark announcement as read.",
+ throwable
+ )
+ }
+ )
+ }
+ },
+ {
+ announcementsMutable.postValue(Error(cause = it))
+ }
+ )
+ }
}
fun addReaction(announcementId: String, name: String) {
- mastodonApi.addAnnouncementReaction(announcementId, name)
- .subscribe(
- {
- announcementsMutable.postValue(
- Success(
- announcements.value!!.data!!.map { announcement ->
- if (announcement.id == announcementId) {
- announcement.copy(
- reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
- announcement.reactions.map { reaction ->
+ viewModelScope.launch {
+ mastodonApi.addAnnouncementReaction(announcementId, name)
+ .fold(
+ {
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
+ announcement.reactions.map { reaction ->
+ if (reaction.name == name) {
+ reaction.copy(
+ count = reaction.count + 1,
+ me = true
+ )
+ } else {
+ reaction
+ }
+ }
+ } else {
+ listOf(
+ *announcement.reactions.toTypedArray(),
+ emojis.value!!.find { emoji -> emoji.shortcode == name }
+ !!.run {
+ Announcement.Reaction(
+ name,
+ 1,
+ true,
+ url,
+ staticUrl
+ )
+ }
+ )
+ }
+ )
+ } else {
+ announcement
+ }
+ }
+ )
+ )
+ },
+ {
+ Log.w(TAG, "Failed to add reaction to the announcement.", it)
+ }
+ )
+ }
+ }
+
+ fun removeReaction(announcementId: String, name: String) {
+ viewModelScope.launch {
+ mastodonApi.removeAnnouncementReaction(announcementId, name)
+ .fold(
+ {
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = announcement.reactions.mapNotNull { reaction ->
if (reaction.name == name) {
- reaction.copy(
- count = reaction.count + 1,
- me = true
- )
+ if (reaction.count > 1) {
+ reaction.copy(
+ count = reaction.count - 1,
+ me = false
+ )
+ } else {
+ null
+ }
} else {
reaction
}
}
- } else {
- listOf(
- *announcement.reactions.toTypedArray(),
- emojis.value!!.find { emoji -> emoji.shortcode == name }
- !!.run {
- Announcement.Reaction(
- name,
- 1,
- true,
- url,
- staticUrl
- )
- }
- )
- }
- )
- } else {
- announcement
+ )
+ } else {
+ announcement
+ }
}
- }
+ )
)
- )
- },
- {
- Log.w(TAG, "Failed to add reaction to the announcement.", it)
- }
- )
- .autoDispose()
- }
-
- fun removeReaction(announcementId: String, name: String) {
- mastodonApi.removeAnnouncementReaction(announcementId, name)
- .subscribe(
- {
- announcementsMutable.postValue(
- Success(
- announcements.value!!.data!!.map { announcement ->
- if (announcement.id == announcementId) {
- announcement.copy(
- reactions = announcement.reactions.mapNotNull { reaction ->
- if (reaction.name == name) {
- if (reaction.count > 1) {
- reaction.copy(
- count = reaction.count - 1,
- me = false
- )
- } else {
- null
- }
- } else {
- reaction
- }
- }
- )
- } else {
- announcement
- }
- }
- )
- )
- },
- {
- Log.w(TAG, "Failed to remove reaction from the announcement.", it)
- }
- )
- .autoDispose()
+ },
+ {
+ Log.w(TAG, "Failed to remove reaction from the announcement.", it)
+ }
+ )
+ }
}
companion object {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
index 2bb97356..a243dcef 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
@@ -51,6 +51,8 @@ import androidx.core.view.ContentInfoCompat
import androidx.core.view.OnReceiveContentListener
import androidx.core.view.isGone
import androidx.core.view.isVisible
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager
@@ -65,6 +67,7 @@ import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
+import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftAttachment
@@ -93,6 +96,7 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.io.File
import java.io.IOException
@@ -123,8 +127,8 @@ class ComposeActivity :
private var photoUploadUri: Uri? = null
@VisibleForTesting
- var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
- var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH
+ var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
+ var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
@@ -328,11 +332,10 @@ class ComposeActivity :
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
withLifecycleContext {
- viewModel.instanceParams.observe { instanceData ->
+ viewModel.instanceInfo.observe { instanceData ->
maximumTootCharacters = instanceData.maxChars
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
updateVisibleCharactersLeft()
- binding.composeScheduleButton.visible(instanceData.supportsScheduled)
}
viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
@@ -342,14 +345,17 @@ class ComposeActivity :
viewModel.statusVisibility.observe { visibility ->
setStatusVisibility(visibility)
}
- viewModel.media.observe { media ->
- mediaAdapter.submitList(media)
- if (media.size != mediaCount) {
- mediaCount = media.size
- binding.composeMediaPreviewBar.visible(media.isNotEmpty())
- updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
+ lifecycleScope.launch {
+ viewModel.media.collect { media ->
+ mediaAdapter.submitList(media)
+ if (media.size != mediaCount) {
+ mediaCount = media.size
+ binding.composeMediaPreviewBar.visible(media.isNotEmpty())
+ updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
+ }
}
}
+
viewModel.poll.observe { poll ->
binding.pollPreview.visible(poll != null)
poll?.let(binding.pollPreview::setPoll)
@@ -362,7 +368,7 @@ class ComposeActivity :
}
updateScheduleButton()
}
- combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
+ combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll ->
val active = poll == null &&
media!!.size != 4 &&
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
@@ -666,7 +672,7 @@ class ComposeActivity :
private fun openPollDialog() {
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
- val instanceParams = viewModel.instanceParams.value!!
+ val instanceParams = viewModel.instanceInfo.value!!
showAddPollDialog(
this, viewModel.poll.value, instanceParams.pollMaxOptions,
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
@@ -779,11 +785,11 @@ class ComposeActivity :
spoilerText = binding.composeContentWarningField.text.toString()
}
val characterCount = calculateTextLength()
- if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) {
+ if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
binding.composeEditField.error = getString(R.string.error_empty)
enableButtons(true)
} else if (characterCount <= maximumTootCharacters) {
- if (viewModel.media.value!!.isNotEmpty()) {
+ if (viewModel.media.value.isNotEmpty()) {
finishingUploadDialog = ProgressDialog.show(
this, getString(R.string.dialog_title_finishing_media_upload),
getString(R.string.dialog_message_uploading_media), true, true
@@ -866,25 +872,15 @@ class ComposeActivity :
}
private fun pickMedia(uri: Uri) {
- withLifecycleContext {
- viewModel.pickMedia(uri).observe { exceptionOrItem ->
- exceptionOrItem.asLeftOrNull()?.let {
- val errorId = when (it) {
- is VideoSizeException -> {
- R.string.error_video_upload_size
- }
- is AudioSizeException -> {
- R.string.error_audio_upload_size
- }
- is VideoOrImageException -> {
- R.string.error_media_upload_image_or_video
- }
- else -> {
- R.string.error_media_upload_opening
- }
- }
- displayTransientError(errorId)
+ lifecycleScope.launch {
+ viewModel.pickMedia(uri).onFailure { throwable ->
+ val errorId = when (throwable) {
+ is VideoSizeException -> R.string.error_video_upload_size
+ is AudioSizeException -> R.string.error_audio_upload_size
+ is VideoOrImageException -> R.string.error_media_upload_image_or_video
+ else -> R.string.error_media_upload_opening
}
+ displayTransientError(errorId)
}
}
}
@@ -971,8 +967,19 @@ class ComposeActivity :
}
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
- viewModel.saveDraft(contentText, contentWarning)
- finishWithoutSlideOutAnimation()
+ lifecycleScope.launch {
+ val dialog = if (viewModel.shouldShowSaveDraftDialog()) {
+ ProgressDialog.show(
+ this@ComposeActivity, null,
+ getString(R.string.saving_draft), true, false
+ )
+ } else {
+ null
+ }
+ viewModel.saveDraft(contentText, contentWarning)
+ dialog?.cancel()
+ finishWithoutSlideOutAnimation()
+ }
}
override fun search(token: String): List {
@@ -991,7 +998,7 @@ class ComposeActivity :
}
data class QueuedMedia(
- val localId: Long,
+ val localId: Int,
val uri: Uri,
val type: Type,
val mediaSize: Long,
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 66dacfb4..7faf1139 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
@@ -20,14 +20,15 @@ import android.util.Log
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.drafts.DraftHelper
+import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
+import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager
-import com.keylesspalace.tusky.db.AppDatabase
-import com.keylesspalace.tusky.db.InstanceEntity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
@@ -35,19 +36,21 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.StatusToSend
-import com.keylesspalace.tusky.util.Either
-import com.keylesspalace.tusky.util.RxAwareViewModel
-import com.keylesspalace.tusky.util.VersionUtils
import com.keylesspalace.tusky.util.combineLiveData
-import com.keylesspalace.tusky.util.filter
-import com.keylesspalace.tusky.util.map
import com.keylesspalace.tusky.util.randomAlphanumericString
import com.keylesspalace.tusky.util.toLiveData
-import com.keylesspalace.tusky.util.withoutFirstWhich
import io.reactivex.rxjava3.core.Observable
-import io.reactivex.rxjava3.core.Single
-import io.reactivex.rxjava3.disposables.Disposable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
+import kotlinx.coroutines.rx3.rxSingle
+import kotlinx.coroutines.withContext
import java.util.Locale
import javax.inject.Inject
@@ -57,8 +60,8 @@ class ComposeViewModel @Inject constructor(
private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper,
- private val db: AppDatabase
-) : RxAwareViewModel() {
+ private val instanceInfoRepo: InstanceInfoRepository
+) : ViewModel() {
private var replyingStatusAuthor: String? = null
private var replyingStatusContent: String? = null
@@ -72,19 +75,8 @@ class ComposeViewModel @Inject constructor(
private var contentWarningStateChanged: Boolean = false
private var modifiedInitialState: Boolean = false
- private val instance: MutableLiveData = MutableLiveData(null)
+ val instanceInfo: MutableLiveData = MutableLiveData()
- val instanceParams: LiveData = instance.map { instance ->
- ComposeInstanceParams(
- maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
- pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
- pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
- pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
- pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
- charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
- supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
- )
- }
val emoji: MutableLiveData?> = MutableLiveData()
val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
@@ -95,131 +87,104 @@ class ComposeViewModel @Inject constructor(
val poll: MutableLiveData = mutableLiveData(null)
val scheduledAt: MutableLiveData = mutableLiveData(null)
- val media = mutableLiveData>(listOf())
+ val media: MutableStateFlow> = MutableStateFlow(emptyList())
val uploadError = MutableLiveData()
- private val mediaToDisposable = mutableMapOf()
+ private val mediaToJob = mutableMapOf()
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
init {
-
- Single.zip(
- api.getCustomEmojis(), api.getInstance()
- ) { emojis, instance ->
- InstanceEntity(
- instance = accountManager.activeAccount?.domain!!,
- emojiList = emojis,
- maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
- maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
- maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
- minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
- maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
- charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
- version = instance.version
- )
+ viewModelScope.launch {
+ emoji.postValue(instanceInfoRepo.getEmojis())
+ }
+ viewModelScope.launch {
+ instanceInfo.postValue(instanceInfoRepo.getInstanceInfo())
}
- .doOnSuccess {
- db.instanceDao().insertOrReplace(it)
- }
- .onErrorResumeNext {
- db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
- }
- .subscribe(
- { instanceEntity ->
- emoji.postValue(instanceEntity.emojiList)
- instance.postValue(instanceEntity)
- },
- { throwable ->
- // this can happen on network error when no cached data is available
- Log.w(TAG, "error loading instance data", throwable)
- }
- )
- .autoDispose()
}
- fun pickMedia(uri: Uri, description: String? = null): LiveData> {
- // We are not calling .toLiveData() here because we don't want to stop the process when
- // the Activity goes away temporarily (like on screen rotation).
- val liveData = MutableLiveData>()
- mediaUploader.prepareMedia(uri)
- .map { (type, uri, size) ->
- val mediaItems = media.value!!
- if (type != QueuedMedia.Type.IMAGE &&
- mediaItems.isNotEmpty() &&
- mediaItems[0].type == QueuedMedia.Type.IMAGE
- ) {
- throw VideoOrImageException()
- } else {
- addMediaToQueue(type, uri, size, description)
- }
+ suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) {
+ try {
+ val (type, uri, size) = mediaUploader.prepareMedia(mediaUri)
+ val mediaItems = media.value
+ if (type != QueuedMedia.Type.IMAGE &&
+ mediaItems.isNotEmpty() &&
+ mediaItems[0].type == QueuedMedia.Type.IMAGE
+ ) {
+ Result.failure(VideoOrImageException())
+ } else {
+ val queuedMedia = addMediaToQueue(type, uri, size, description)
+ Result.success(queuedMedia)
}
- .subscribe(
- { queuedMedia ->
- liveData.postValue(Either.Right(queuedMedia))
- },
- { error ->
- liveData.postValue(Either.Left(error))
- }
- )
- .autoDispose()
- return liveData
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
}
- private fun addMediaToQueue(
+ private suspend fun addMediaToQueue(
type: QueuedMedia.Type,
uri: Uri,
mediaSize: Long,
description: String? = null
): QueuedMedia {
- val mediaItem = QueuedMedia(
- localId = System.currentTimeMillis(),
- uri = uri,
- type = type,
- mediaSize = mediaSize,
- description = description
- )
- media.value = media.value!! + mediaItem
- mediaToDisposable[mediaItem.localId] = mediaUploader
- .uploadMedia(mediaItem)
- .subscribe(
- { event ->
- val item = media.value?.find { it.localId == mediaItem.localId }
- ?: return@subscribe
+ val mediaItem = media.updateAndGet { mediaValue ->
+ val mediaItem = QueuedMedia(
+ localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
+ uri = uri,
+ type = type,
+ mediaSize = mediaSize,
+ description = description
+ )
+ mediaValue + mediaItem
+ }.last()
+ mediaToJob[mediaItem.localId] = viewModelScope.launch {
+ mediaUploader
+ .uploadMedia(mediaItem)
+ .catch { error ->
+ media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
+ uploadError.postValue(error)
+ }
+ .collect { event ->
+ val item = media.value.find { it.localId == mediaItem.localId }
+ ?: return@collect
val newMediaItem = when (event) {
is UploadEvent.ProgressEvent ->
item.copy(uploadPercent = event.percentage)
is UploadEvent.FinishedEvent ->
item.copy(id = event.mediaId, uploadPercent = -1)
}
- synchronized(media) {
- val mediaValue = media.value!!
- val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
- media.postValue(
- if (index == -1) {
- mediaValue + newMediaItem
+ media.update { mediaValue ->
+ mediaValue.map { mediaItem ->
+ if (mediaItem.localId == newMediaItem.localId) {
+ newMediaItem
} else {
- mediaValue.toMutableList().also { it[index] = newMediaItem }
+ mediaItem
}
- )
+ }
}
- },
- { error ->
- media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
- uploadError.postValue(error)
}
- )
+ }
return mediaItem
}
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
- val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description)
- media.value = media.value!! + mediaItem
+ media.update { mediaValue ->
+ val mediaItem = QueuedMedia(
+ localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
+ uri = uri,
+ type = type,
+ mediaSize = 0,
+ uploadPercent = -1,
+ id = id,
+ description = description
+ )
+ mediaValue + mediaItem
+ }
}
fun removeMediaFromQueue(item: QueuedMedia) {
- mediaToDisposable[item.localId]?.dispose()
- media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
+ mediaToJob[item.localId]?.cancel()
+ media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } }
}
fun toggleMarkSensitive() {
@@ -255,31 +220,36 @@ class ComposeViewModel @Inject constructor(
}
}
- fun saveDraft(content: String, contentWarning: String) {
- viewModelScope.launch {
- val mediaUris: MutableList = mutableListOf()
- val mediaDescriptions: MutableList = mutableListOf()
- media.value?.forEach { item ->
- mediaUris.add(item.uri.toString())
- mediaDescriptions.add(item.description)
- }
-
- draftHelper.saveDraft(
- draftId = draftId,
- accountId = accountManager.activeAccount?.id!!,
- inReplyToId = inReplyToId,
- content = content,
- contentWarning = contentWarning,
- sensitive = markMediaAsSensitive.value!!,
- visibility = statusVisibility.value!!,
- mediaUris = mediaUris,
- mediaDescriptions = mediaDescriptions,
- poll = poll.value,
- failedToSend = false
- )
+ fun shouldShowSaveDraftDialog(): Boolean {
+ // if any of the media files need to be downloaded first it could take a while, so show a loading dialog
+ return media.value.any { mediaValue ->
+ mediaValue.uri.scheme == "https"
}
}
+ suspend fun saveDraft(content: String, contentWarning: String) {
+ val mediaUris: MutableList = mutableListOf()
+ val mediaDescriptions: MutableList = mutableListOf()
+ media.value.forEach { item ->
+ mediaUris.add(item.uri.toString())
+ mediaDescriptions.add(item.description)
+ }
+
+ draftHelper.saveDraft(
+ draftId = draftId,
+ accountId = accountManager.activeAccount?.id!!,
+ inReplyToId = inReplyToId,
+ content = content,
+ contentWarning = contentWarning,
+ sensitive = markMediaAsSensitive.value!!,
+ visibility = statusVisibility.value!!,
+ mediaUris = mediaUris,
+ mediaDescriptions = mediaDescriptions,
+ poll = poll.value,
+ failedToSend = false
+ )
+ }
+
/**
* Send status to the server.
* Uses current state plus provided arguments.
@@ -291,21 +261,23 @@ 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()
- val sendObservable = media
+ val sendFlow = media
.filter { items -> items.all { it.uploadPercent == -1 } }
.map {
- val mediaIds = ArrayList()
- val mediaUris = ArrayList()
- val mediaDescriptions = ArrayList()
- for (item in media.value!!) {
+ val mediaIds: MutableList = mutableListOf()
+ val mediaUris: MutableList = mutableListOf()
+ val mediaDescriptions: MutableList = mutableListOf()
+ val mediaProcessed: MutableList = mutableListOf()
+ for (item in media.value) {
mediaIds.add(item.id!!)
mediaUris.add(item.uri)
mediaDescriptions.add(item.description ?: "")
+ mediaProcessed.add(false)
}
val tootToSend = StatusToSend(
@@ -324,44 +296,38 @@ class ComposeViewModel @Inject constructor(
accountId = accountManager.activeAccount!!.id,
draftId = draftId,
idempotencyKey = randomAlphanumericString(16),
- retries = 0
+ retries = 0,
+ mediaProcessed = mediaProcessed
)
serviceClient.sendToot(tootToSend)
}
- return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
+ return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> }
}
- fun updateDescription(localId: Long, description: String): LiveData {
- val newList = media.value!!.toMutableList()
- val index = newList.indexOfFirst { it.localId == localId }
- if (index != -1) {
- newList[index] = newList[index].copy(description = description)
- }
- media.value = newList
- val completedCaptioningLiveData = MutableLiveData()
- media.observeForever(object : Observer> {
- override fun onChanged(mediaItems: List) {
- val updatedItem = mediaItems.find { it.localId == localId }
- if (updatedItem == null) {
- media.removeObserver(this)
- } else if (updatedItem.id != null) {
- api.updateMedia(updatedItem.id, description)
- .subscribe(
- {
- completedCaptioningLiveData.postValue(true)
- },
- {
- completedCaptioningLiveData.postValue(false)
- }
- )
- .autoDispose()
- media.removeObserver(this)
+ suspend fun updateDescription(localId: Int, description: String): Boolean {
+ val newMediaList = media.updateAndGet { mediaValue ->
+ mediaValue.map { mediaItem ->
+ if (mediaItem.localId == localId) {
+ mediaItem.copy(description = description)
+ } else {
+ mediaItem
}
}
- })
- return completedCaptioningLiveData
+ }
+
+ val updatedItem = newMediaList.find { it.localId == localId }
+ if (updatedItem?.id != null) {
+ return api.updateMedia(updatedItem.id, description)
+ .fold({
+ true
+ }, { throwable ->
+ Log.w(TAG, "failed to update media", throwable)
+ false
+ })
+ }
+ return true
}
fun searchAutocompleteSuggestions(token: String): List {
@@ -443,7 +409,11 @@ class ComposeViewModel @Inject constructor(
val draftAttachments = composeOptions?.draftAttachments
if (draftAttachments != null) {
// when coming from DraftActivity
- draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
+ viewModelScope.launch {
+ draftAttachments.forEach { attachment ->
+ pickMedia(attachment.uri, attachment.description)
+ }
+ }
} else composeOptions?.mediaAttachments?.forEach { a ->
// when coming from redraft or ScheduledTootActivity
val mediaType = when (a.type) {
@@ -494,13 +464,6 @@ class ComposeViewModel @Inject constructor(
scheduledAt.value = newScheduledAt
}
- override fun onCleared() {
- for (uploadDisposable in mediaToDisposable.values) {
- uploadDisposable.dispose()
- }
- super.onCleared()
- }
-
private companion object {
const val TAG = "ComposeViewModel"
}
@@ -508,25 +471,6 @@ class ComposeViewModel @Inject constructor(
fun mutableLiveData(default: T) = MutableLiveData().apply { value = default }
-const val DEFAULT_CHARACTER_LIMIT = 500
-private const val DEFAULT_MAX_OPTION_COUNT = 4
-private const val DEFAULT_MAX_OPTION_LENGTH = 50
-private const val DEFAULT_MIN_POLL_DURATION = 300
-private const val DEFAULT_MAX_POLL_DURATION = 604800
-
-// Mastodon only counts URLs as this long in terms of status character limits
-const val DEFAULT_MAXIMUM_URL_LENGTH = 23
-
-data class ComposeInstanceParams(
- val maxChars: Int,
- val pollMaxOptions: Int,
- val pollMaxLength: Int,
- val pollMinDuration: Int,
- val pollMaxDuration: Int,
- val charactersReservedPerUrl: Int,
- val supportsScheduled: Boolean
-)
-
/**
* Thrown when trying to add an image when video is already present or the other way around
*/
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java
deleted file mode 100644
index 880a4167..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * 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.compose;
-
-import android.content.ContentResolver;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.AsyncTask;
-
-import com.keylesspalace.tusky.util.IOUtils;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize;
-import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation;
-import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap;
-
-/**
- * Reduces the file size of images to fit under a given limit by resizing them, maintaining both
- * aspect ratio and orientation.
- */
-public class DownsizeImageTask extends AsyncTask {
- private int sizeLimit;
- private ContentResolver contentResolver;
- private Listener listener;
- private File tempFile;
-
- /**
- * @param sizeLimit the maximum number of bytes each image can take
- * @param contentResolver to resolve the specified images' URIs
- * @param tempFile the file where the result will be stored
- * @param listener to whom the results are given
- */
- public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
- this.sizeLimit = sizeLimit;
- this.contentResolver = contentResolver;
- this.tempFile = tempFile;
- this.listener = listener;
- }
-
- @Override
- protected Boolean doInBackground(Uri... uris) {
- boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile);
- if (isCancelled()) {
- return false;
- }
- return result;
- }
-
- @Override
- protected void onPostExecute(Boolean successful) {
- if (successful) {
- listener.onSuccess(tempFile);
- } else {
- listener.onFailure();
- }
- super.onPostExecute(successful);
- }
-
- public static boolean resize(Uri[] uris, int sizeLimit, ContentResolver contentResolver,
- File tempFile) {
- for (Uri uri : uris) {
- InputStream inputStream;
- try {
- inputStream = contentResolver.openInputStream(uri);
- } catch (FileNotFoundException e) {
- return false;
- }
- // Initially, just get the image dimensions.
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeStream(inputStream, null, options);
- IOUtils.closeQuietly(inputStream);
- // Get EXIF data, for orientation info.
- int orientation = getImageOrientation(uri, contentResolver);
- /* Unfortunately, there isn't a determined worst case compression ratio for image
- * formats. So, the only way to tell if they're too big is to compress them and
- * test, and keep trying at smaller sizes. The initial estimate should be good for
- * many cases, so it should only iterate once, but the loop is used to be absolutely
- * sure it gets downsized to below the limit. */
- int scaledImageSize = 1024;
- do {
- OutputStream stream;
- try {
- stream = new FileOutputStream(tempFile);
- } catch (FileNotFoundException e) {
- return false;
- }
- try {
- inputStream = contentResolver.openInputStream(uri);
- } catch (FileNotFoundException e) {
- return false;
- }
- options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize);
- options.inJustDecodeBounds = false;
- Bitmap scaledBitmap;
- try {
- scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options);
- } catch (OutOfMemoryError error) {
- return false;
- } finally {
- IOUtils.closeQuietly(inputStream);
- }
- if (scaledBitmap == null) {
- return false;
- }
- Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
- if (reorientedBitmap == null) {
- scaledBitmap.recycle();
- return false;
- }
- Bitmap.CompressFormat format;
- /* It's not likely the user will give transparent images over the upload limit, but
- * if they do, make sure the transparency is retained. */
- if (!reorientedBitmap.hasAlpha()) {
- format = Bitmap.CompressFormat.JPEG;
- } else {
- format = Bitmap.CompressFormat.PNG;
- }
- reorientedBitmap.compress(format, 85, stream);
- reorientedBitmap.recycle();
- scaledImageSize /= 2;
- } while (tempFile.length() > sizeLimit);
- }
- return true;
- }
-
- /**
- * Used to communicate the results of the task.
- */
- public interface Listener {
- void onSuccess(File file);
-
- void onFailure();
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt
new file mode 100644
index 00000000..a0215847
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt
@@ -0,0 +1,101 @@
+/* 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.compose
+
+import android.content.ContentResolver
+import android.graphics.Bitmap
+import android.graphics.Bitmap.CompressFormat
+import android.graphics.BitmapFactory
+import android.net.Uri
+import com.keylesspalace.tusky.util.IOUtils
+import com.keylesspalace.tusky.util.calculateInSampleSize
+import com.keylesspalace.tusky.util.getImageOrientation
+import com.keylesspalace.tusky.util.reorientBitmap
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+
+/**
+ * @param uri the uri pointing to the input file
+ * @param sizeLimit the maximum number of bytes the output image is allowed to have
+ * @param contentResolver to resolve the specified input uri
+ * @param tempFile the file where the result will be stored
+ * @return true when the image was successfully resized, false otherwise
+ */
+fun downsizeImage(
+ uri: Uri,
+ sizeLimit: Int,
+ contentResolver: ContentResolver,
+ tempFile: File
+): Boolean {
+
+ val decodeBoundsInputStream = try {
+ contentResolver.openInputStream(uri)
+ } catch (e: FileNotFoundException) {
+ return false
+ }
+ // Initially, just get the image dimensions.
+ val options = BitmapFactory.Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeStream(decodeBoundsInputStream, null, options)
+ IOUtils.closeQuietly(decodeBoundsInputStream)
+ // Get EXIF data, for orientation info.
+ val orientation = getImageOrientation(uri, contentResolver)
+ /* Unfortunately, there isn't a determined worst case compression ratio for image
+ * formats. So, the only way to tell if they're too big is to compress them and
+ * test, and keep trying at smaller sizes. The initial estimate should be good for
+ * many cases, so it should only iterate once, but the loop is used to be absolutely
+ * sure it gets downsized to below the limit. */
+ var scaledImageSize = 1024
+ do {
+ val outputStream = try {
+ FileOutputStream(tempFile)
+ } catch (e: FileNotFoundException) {
+ return false
+ }
+ val decodeBitmapInputStream = try {
+ contentResolver.openInputStream(uri)
+ } catch (e: FileNotFoundException) {
+ return false
+ }
+ options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize)
+ options.inJustDecodeBounds = false
+ val scaledBitmap: Bitmap = try {
+ BitmapFactory.decodeStream(decodeBitmapInputStream, null, options)
+ } catch (error: OutOfMemoryError) {
+ return false
+ } finally {
+ IOUtils.closeQuietly(decodeBitmapInputStream)
+ } ?: return false
+
+ val reorientedBitmap = reorientBitmap(scaledBitmap, orientation)
+ if (reorientedBitmap == null) {
+ scaledBitmap.recycle()
+ return false
+ }
+ /* Retain transparency if there is any by encoding as png */
+ val format: CompressFormat = if (!reorientedBitmap.hasAlpha()) {
+ CompressFormat.JPEG
+ } else {
+ CompressFormat.PNG
+ }
+ reorientedBitmap.compress(format, 85, outputStream)
+ reorientedBitmap.recycle()
+ scaledImageSize /= 2
+ } while (tempFile.length() > sizeLimit)
+
+ return true
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
index 0e3ac9e8..f1debc98 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
@@ -26,15 +26,20 @@ import androidx.core.net.toUri
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
-import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.network.MediaUploadApi
import com.keylesspalace.tusky.network.ProgressRequestBody
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
import com.keylesspalace.tusky.util.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.randomAlphanumericString
-import io.reactivex.rxjava3.core.Observable
-import io.reactivex.rxjava3.core.Single
-import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import java.io.File
@@ -70,63 +75,42 @@ class CouldNotOpenFileException : Exception()
class MediaUploader @Inject constructor(
private val context: Context,
- private val mastodonApi: MastodonApi
+ private val mediaUploadApi: MediaUploadApi
) {
- fun uploadMedia(media: QueuedMedia): Observable {
- return Observable
- .fromCallable {
- if (shouldResizeMedia(media)) {
- downsize(media)
- } else media
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun uploadMedia(media: QueuedMedia): Flow {
+ return flow {
+ if (shouldResizeMedia(media)) {
+ emit(downsize(media))
+ } else {
+ emit(media)
}
- .switchMap { upload(it) }
- .subscribeOn(Schedulers.io())
+ }
+ .flatMapLatest { upload(it) }
+ .flowOn(Dispatchers.IO)
}
- fun prepareMedia(inUri: Uri): Single {
- return Single.fromCallable {
- var mediaSize = MEDIA_SIZE_UNKNOWN
- var uri = inUri
- var mimeType: String? = null
+ fun prepareMedia(inUri: Uri): PreparedMedia {
+ var mediaSize = MEDIA_SIZE_UNKNOWN
+ var uri = inUri
+ val mimeType: String?
- try {
- when (inUri.scheme) {
- ContentResolver.SCHEME_CONTENT -> {
+ try {
+ when (inUri.scheme) {
+ ContentResolver.SCHEME_CONTENT -> {
- mimeType = contentResolver.getType(uri)
+ mimeType = contentResolver.getType(uri)
- val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
+ val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
- contentResolver.openInputStream(inUri).use { input ->
- if (input == null) {
- Log.w(TAG, "Media input is null")
- uri = inUri
- return@use
- }
- val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
- FileOutputStream(file.absoluteFile).use { out ->
- input.copyTo(out)
- uri = FileProvider.getUriForFile(
- context,
- BuildConfig.APPLICATION_ID + ".fileprovider",
- file
- )
- mediaSize = getMediaSize(contentResolver, uri)
- }
+ contentResolver.openInputStream(inUri).use { input ->
+ if (input == null) {
+ Log.w(TAG, "Media input is null")
+ uri = inUri
+ return@use
}
- }
- ContentResolver.SCHEME_FILE -> {
- val path = uri.path
- if (path == null) {
- Log.w(TAG, "empty uri path $uri")
- throw CouldNotOpenFileException()
- }
- val inputFile = File(path)
- val suffix = inputFile.name.substringAfterLast('.', "tmp")
- mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
- val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
- val input = FileInputStream(inputFile)
-
+ val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(
@@ -137,53 +121,74 @@ class MediaUploader @Inject constructor(
mediaSize = getMediaSize(contentResolver, uri)
}
}
- else -> {
- Log.w(TAG, "Unknown uri scheme $uri")
+ }
+ ContentResolver.SCHEME_FILE -> {
+ val path = uri.path
+ if (path == null) {
+ Log.w(TAG, "empty uri path $uri")
throw CouldNotOpenFileException()
}
- }
- } catch (e: IOException) {
- Log.w(TAG, e)
- throw CouldNotOpenFileException()
- }
- if (mediaSize == MEDIA_SIZE_UNKNOWN) {
- Log.w(TAG, "Could not determine file size of upload")
- throw MediaTypeException()
- }
+ val inputFile = File(path)
+ val suffix = inputFile.name.substringAfterLast('.', "tmp")
+ mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
+ val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
+ val input = FileInputStream(inputFile)
- if (mimeType != null) {
- val topLevelType = mimeType.substring(0, mimeType.indexOf('/'))
- when (topLevelType) {
- "video" -> {
- if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
- throw VideoSizeException()
- }
- PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
- }
- "image" -> {
- PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
- }
- "audio" -> {
- if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
- throw AudioSizeException()
- }
- PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
- }
- else -> {
- throw MediaTypeException()
+ FileOutputStream(file.absoluteFile).use { out ->
+ input.copyTo(out)
+ uri = FileProvider.getUriForFile(
+ context,
+ BuildConfig.APPLICATION_ID + ".fileprovider",
+ file
+ )
+ mediaSize = getMediaSize(contentResolver, uri)
}
}
- } else {
- Log.w(TAG, "Could not determine mime type of upload")
- throw MediaTypeException()
+ else -> {
+ Log.w(TAG, "Unknown uri scheme $uri")
+ throw CouldNotOpenFileException()
+ }
}
+ } catch (e: IOException) {
+ Log.w(TAG, e)
+ throw CouldNotOpenFileException()
+ }
+ if (mediaSize == MEDIA_SIZE_UNKNOWN) {
+ Log.w(TAG, "Could not determine file size of upload")
+ throw MediaTypeException()
+ }
+
+ if (mimeType != null) {
+ return when (mimeType.substring(0, mimeType.indexOf('/'))) {
+ "video" -> {
+ if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
+ throw VideoSizeException()
+ }
+ PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
+ }
+ "image" -> {
+ PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
+ }
+ "audio" -> {
+ if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
+ throw AudioSizeException()
+ }
+ PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
+ }
+ else -> {
+ throw MediaTypeException()
+ }
+ }
+ } else {
+ Log.w(TAG, "Could not determine mime type of upload")
+ throw MediaTypeException()
}
}
private val contentResolver = context.contentResolver
- private fun upload(media: QueuedMedia): Observable {
- return Observable.create { emitter ->
+ private suspend fun upload(media: QueuedMedia): Flow {
+ return callbackFlow {
var mimeType = contentResolver.getType(media.uri)
val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType)
@@ -200,11 +205,11 @@ class MediaUploader @Inject constructor(
var lastProgress = -1
val fileBody = ProgressRequestBody(
- stream, media.mediaSize,
- mimeType.toMediaTypeOrNull()
+ stream!!, media.mediaSize,
+ mimeType.toMediaTypeOrNull()!!
) { percentage ->
if (percentage != lastProgress) {
- emitter.onNext(UploadEvent.ProgressEvent(percentage))
+ trySend(UploadEvent.ProgressEvent(percentage))
}
lastProgress = percentage
}
@@ -217,28 +222,15 @@ class MediaUploader @Inject constructor(
null
}
- val uploadDisposable = mastodonApi.uploadMedia(body, description)
- .subscribe(
- { result ->
- emitter.onNext(UploadEvent.FinishedEvent(result.id))
- emitter.onComplete()
- },
- { e ->
- emitter.onError(e)
- }
- )
-
- // Cancel the request when our observable is cancelled
- emitter.setDisposable(uploadDisposable)
+ val result = mediaUploadApi.uploadMedia(body, description).getOrThrow()
+ send(UploadEvent.FinishedEvent(result.id))
+ awaitClose()
}
}
private fun downsize(media: QueuedMedia): QueuedMedia {
val file = createNewImageFile(context)
- DownsizeImageTask.resize(
- arrayOf(media.uri),
- STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
- )
+ downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file)
return media.copy(uri = file.toUri(), mediaSize = file.length())
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt
index 0c15eff0..71789611 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt
@@ -27,7 +27,7 @@ import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LiveData
+import androidx.lifecycle.lifecycleScope
import at.connyduck.sparkbutton.helpers.Utils
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
@@ -35,7 +35,7 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.github.chrisbanes.photoview.PhotoView
import com.keylesspalace.tusky.R
-import com.keylesspalace.tusky.util.withLifecycleContext
+import kotlinx.coroutines.launch
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
@@ -43,7 +43,7 @@ private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun T.makeCaptionDialog(
existingDescription: String?,
previewUri: Uri,
- onUpdateDescription: (String) -> LiveData
+ onUpdateDescription: suspend (String) -> Boolean
) where T : Activity, T : LifecycleOwner {
val dialogLayout = LinearLayout(this)
val padding = Utils.dpToPx(this, 8)
@@ -77,12 +77,11 @@ fun T.makeCaptionDialog(
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
val okListener = { dialog: DialogInterface, _: Int ->
- onUpdateDescription(input.text.toString())
- withLifecycleContext {
- onUpdateDescription(input.text.toString())
- .observe { success -> if (!success) showFailedCaptionMessage() }
+ lifecycleScope.launch {
+ if (!onUpdateDescription(input.text.toString())) {
+ showFailedCaptionMessage()
+ }
}
-
dialog.dismiss()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt
index dca696d8..2a1c7446 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt
@@ -26,7 +26,7 @@ import androidx.core.view.OnReceiveContentListener
import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
-import androidx.emoji.widget.EmojiEditTextHelper
+import androidx.emoji2.viewsintegration.EmojiEditTextHelper
class EditTextTyped @JvmOverloads constructor(
context: Context,
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 89c1ad0f..0c946514 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 88c9dbad..f585b4ea 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,95 +88,43 @@ 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
+ 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
+ ),
+ isExpanded = expanded,
+ isShowingContent = showingHiddenContent,
+ isCollapsed = collapsed
)
}
}
@@ -202,7 +158,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 00000000..470675d1
--- /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 436ba84e..ffb88a94 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(),
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 21c6fef6..2f1c0366 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
@@ -153,24 +153,24 @@ 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)
}
}
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)
@@ -189,14 +189,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)
}
}
@@ -205,13 +205,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)
}
}
@@ -221,7 +221,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)
}
}
@@ -241,12 +241,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)
@@ -268,7 +268,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 396f8e48..9326a05c 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/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
index 7511dc3c..a6cd3fcd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
@@ -30,7 +30,12 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.IOUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okio.buffer
+import okio.sink
import java.io.File
+import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -38,6 +43,7 @@ import javax.inject.Inject
class DraftHelper @Inject constructor(
val context: Context,
+ val okHttpClient: OkHttpClient,
db: AppDatabase
) {
@@ -71,11 +77,11 @@ class DraftHelper @Inject constructor(
val uris = mediaUris.map { uriString ->
uriString.toUri()
- }.map { uri ->
- if (uri.isNotInFolder(draftDirectory)) {
- uri.copyToFolder(draftDirectory)
- } else {
+ }.mapNotNull { uri ->
+ if (uri.isInFolder(draftDirectory)) {
uri
+ } else {
+ uri.copyToFolder(draftDirectory)
}
}
@@ -114,6 +120,7 @@ class DraftHelper @Inject constructor(
)
draftDao.insertOrReplace(draft)
+ Log.d("DraftHelper", "saved draft to db")
}
suspend fun deleteDraftAndAttachments(draftId: Int) {
@@ -133,33 +140,55 @@ class DraftHelper @Inject constructor(
}
}
- suspend fun deleteAttachments(draft: DraftEntity) {
- withContext(Dispatchers.IO) {
- draft.attachments.forEach { attachment ->
- if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
- Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
- }
+ suspend fun deleteAttachments(draft: DraftEntity) = withContext(Dispatchers.IO) {
+ draft.attachments.forEach { attachment ->
+ if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
+ Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
}
}
}
- private fun Uri.isNotInFolder(folder: File): Boolean {
+ private fun Uri.isInFolder(folder: File): Boolean {
val filePath = path ?: return true
return File(filePath).parentFile == folder
}
- private fun Uri.copyToFolder(folder: File): Uri {
+ private fun Uri.copyToFolder(folder: File): Uri? {
val contentResolver = context.contentResolver
-
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
- val mimeType = contentResolver.getType(this)
- val map = MimeTypeMap.getSingleton()
- val fileExtension = map.getExtensionFromMimeType(mimeType)
+ val fileExtension = if (scheme == "https") {
+ lastPathSegment?.substringAfterLast('.', "tmp")
+ } else {
+ val mimeType = contentResolver.getType(this)
+ val map = MimeTypeMap.getSingleton()
+ map.getExtensionFromMimeType(mimeType)
+ }
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
val file = File(folder, filename)
- IOUtils.copyToFile(contentResolver, this, file)
+
+ if (scheme == "https") {
+ // saving redrafted media
+ try {
+ val request = Request.Builder().url(toString()).build()
+
+ val response = okHttpClient.newCall(request).execute()
+
+ val sink = file.sink().buffer()
+
+ response.body?.source()?.use { input ->
+ sink.use { output ->
+ output.writeAll(input)
+ }
+ }
+ } catch (ex: IOException) {
+ Log.w("DraftHelper", "failed to save media", ex)
+ return null
+ }
+ } else {
+ IOUtils.copyToFile(contentResolver, this, file)
+ }
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
index e580f554..db6a8a31 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
@@ -35,6 +35,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.visible
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.flow.collectLatest
@@ -100,7 +101,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
content = draft.content,
contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId,
- replyingStatusContent = status.content.toString(),
+ replyingStatusContent = status.content.parseAsMastodonHtml().toString(),
replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments,
poll = draft.poll,
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt
new file mode 100644
index 00000000..05e10b6b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt
@@ -0,0 +1,25 @@
+/* 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.instanceinfo
+
+data class InstanceInfo(
+ val maxChars: Int,
+ val pollMaxOptions: Int,
+ val pollMaxLength: Int,
+ val pollMinDuration: Int,
+ val pollMaxDuration: Int,
+ val charactersReservedPerUrl: Int
+)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt
new file mode 100644
index 00000000..8ed26d7b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt
@@ -0,0 +1,102 @@
+/* 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.instanceinfo
+
+import android.util.Log
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.db.AppDatabase
+import com.keylesspalace.tusky.db.EmojisEntity
+import com.keylesspalace.tusky.db.InstanceInfoEntity
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.network.MastodonApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class InstanceInfoRepository @Inject constructor(
+ private val api: MastodonApi,
+ db: AppDatabase,
+ accountManager: AccountManager
+) {
+
+ private val dao = db.instanceDao()
+ private val instanceName = accountManager.activeAccount!!.domain
+
+ /**
+ * Returns the custom emojis of the instance.
+ * Will always try to fetch them from the api, falls back to cached Emojis in case it is not available.
+ * Never throws, returns empty list in case of error.
+ */
+ suspend fun getEmojis(): List = withContext(Dispatchers.IO) {
+ api.getCustomEmojis()
+ .onSuccess { emojiList -> dao.insertOrReplace(EmojisEntity(instanceName, emojiList)) }
+ .getOrElse { throwable ->
+ Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable)
+ dao.getEmojiInfo(instanceName)?.emojiList.orEmpty()
+ }
+ }
+
+ /**
+ * Returns information about the instance.
+ * Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available.
+ * Never throws, returns defaults of vanilla Mastodon in case of error.
+ */
+ suspend fun getInstanceInfo(): InstanceInfo = withContext(Dispatchers.IO) {
+ api.getInstance()
+ .fold(
+ { instance ->
+ val instanceEntity = InstanceInfoEntity(
+ instance = instanceName,
+ maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
+ maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
+ maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
+ minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
+ maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
+ charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
+ version = instance.version
+ )
+ dao.insertOrReplace(instanceEntity)
+ instanceEntity
+ },
+ { throwable ->
+ Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
+ dao.getInstanceInfo(instanceName)
+ }
+ ).let { instanceInfo: InstanceInfoEntity? ->
+ InstanceInfo(
+ maxChars = instanceInfo?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
+ pollMaxOptions = instanceInfo?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
+ pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
+ pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
+ pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
+ charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL
+ )
+ }
+ }
+
+ companion object {
+ private const val TAG = "InstanceInfoRepo"
+
+ const val DEFAULT_CHARACTER_LIMIT = 500
+ private const val DEFAULT_MAX_OPTION_COUNT = 4
+ private const val DEFAULT_MAX_OPTION_LENGTH = 50
+ private const val DEFAULT_MIN_POLL_DURATION = 300
+ private const val DEFAULT_MAX_POLL_DURATION = 604800
+
+ // Mastodon only counts URLs as this long in terms of status character limits
+ const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23
+ }
+}
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 d8a52a27..e52da81c 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
@@ -34,7 +34,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.rickRoll
@@ -70,7 +69,9 @@ class LoginActivity : BaseActivity(), Injectable {
// Authorization failed. Put the error response where the user can read it and they
// can try again.
setLoading(false)
- binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
+ // Use error returned by the server or fall back to the generic message
+ binding.domainTextInputLayout.error =
+ result.errorMessage.ifBlank { getString(R.string.error_authorization_denied) }
Log.e(
TAG,
"%s %s".format(
@@ -180,32 +181,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
+ }
+ )
}
}
@@ -238,29 +240,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 01f6c3b0..2ed38720 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,10 +16,13 @@ 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
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding
import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.viewBinding
import kotlinx.parcelize.Parcelize
@@ -75,7 +78,7 @@ sealed class LoginResult : Parcelable {
/** Activity to do Oauth process using WebView. */
class LoginWebViewActivity : BaseActivity(), Injectable {
- private val binding by viewBinding(LoginWebviewBinding::inflate)
+ private val binding by viewBinding(ActivityLoginWebviewBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -86,7 +89,9 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
setSupportActionBar(binding.loginToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setDisplayShowTitleEnabled(false)
+ supportActionBar?.setDisplayShowTitleEnabled(true)
+
+ setTitle(R.string.title_login)
val webView = binding.loginWebView
webView.settings.allowContentAccess = false
@@ -102,20 +107,34 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
val oauthUrl = data.oauthRedirectUrl
webView.webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ binding.loginProgress.hide()
+ }
+
override fun onReceivedError(
- view: WebView?,
- request: WebResourceRequest?,
+ view: WebView,
+ request: WebResourceRequest,
error: WebResourceError
) {
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
- finish()
+ sendResult(LoginResult.Err(getString(R.string.error_could_not_load_login_page)))
}
override fun shouldOverrideUrlLoading(
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 +149,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
}
}
}
+
webView.setBackgroundColor(Color.TRANSPARENT)
if (savedInstanceState == null) {
@@ -153,10 +173,14 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
super.onDestroy()
}
+ override fun finish() {
+ super.finishWithoutSlideOutAnimation()
+ }
+
override fun requiresLogin() = false
private fun sendResult(result: LoginResult) {
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
- finish()
+ finishWithoutSlideOutAnimation()
}
}
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 6b9afce1..79586897 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,8 @@ 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";
+ public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
/**
* WorkManager Tag
@@ -340,7 +343,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 +395,8 @@ public class NotificationHelper {
CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(),
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
+ CHANNEL_SIGN_UP + account.getIdentifier(),
+ CHANNEL_UPDATES + account.getIdentifier(),
};
int[] channelNames = {
R.string.notification_mention_name,
@@ -401,6 +406,8 @@ public class NotificationHelper {
R.string.notification_favourite_name,
R.string.notification_poll_name,
R.string.notification_subscription_name,
+ R.string.notification_sign_up_name,
+ R.string.notification_update_name,
};
int[] channelDescriptions = {
R.string.notification_mention_descriptions,
@@ -410,6 +417,8 @@ public class NotificationHelper {
R.string.notification_favourite_description,
R.string.notification_poll_description,
R.string.notification_subscription_description,
+ R.string.notification_sign_up_description,
+ R.string.notification_update_description,
};
List channels = new ArrayList<>(6);
@@ -560,6 +569,10 @@ public class NotificationHelper {
return account.getNotificationsFavorited();
case POLL:
return account.getNotificationsPolls();
+ case SIGN_UP:
+ return account.getNotificationsSignUps();
+ case UPDATE:
+ return account.getNotificationsUpdates();
default:
return false;
}
@@ -582,6 +595,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 +678,10 @@ 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);
+ case UPDATE:
+ return String.format(context.getString(R.string.notification_update_format), accountName);
}
return null;
}
@@ -671,6 +690,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 +699,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
deleted file mode 100644
index 47cb37ae..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
+++ /dev/null
@@ -1,240 +0,0 @@
-package com.keylesspalace.tusky.components.preference
-
-import android.app.AlarmManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.widget.RadioButton
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.preference.Preference
-import androidx.preference.PreferenceManager
-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
-import com.keylesspalace.tusky.util.EmojiCompatFont
-import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI
-import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
-import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI
-import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT
-import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
-import com.keylesspalace.tusky.util.hide
-import com.keylesspalace.tusky.util.show
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
-import io.reactivex.rxjava3.disposables.Disposable
-import okhttp3.OkHttpClient
-import kotlin.system.exitProcess
-
-/**
- * This Preference lets the user select their preferred emoji font
- */
-class EmojiPreference(
- context: Context,
- private val okHttpClient: OkHttpClient
-) : Preference(context) {
-
- private lateinit var selected: EmojiCompatFont
- private lateinit var original: EmojiCompatFont
- private val radioButtons = mutableListOf()
- private var updated = false
- private var currentNeedsUpdate = false
-
- private val downloadDisposables = MutableList(FONTS.size) { null }
-
- override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
- super.onAttachedToHierarchy(preferenceManager)
-
- // Find out which font is currently active
- selected = EmojiCompatFont.byId(
- PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
- )
- // We'll use this later to determine if anything has changed
- original = selected
- summary = selected.getDisplay(context)
- }
-
- override fun onClick() {
- val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context))
-
- setupItem(BLOBMOJI, binding.itemBlobmoji)
- setupItem(TWEMOJI, binding.itemTwemoji)
- setupItem(NOTOEMOJI, binding.itemNotoemoji)
- setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
-
- AlertDialog.Builder(context)
- .setView(binding.root)
- .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
-
- private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
- // Initialize all the views
- binding.emojiName.text = font.getDisplay(context)
- binding.emojiCaption.setText(font.caption)
- binding.emojiThumbnail.setImageResource(font.img)
-
- // There needs to be a list of all the radio buttons in order to uncheck them when one is selected
- radioButtons.add(binding.emojiRadioButton)
- updateItem(font, binding)
-
- // Set actions
- binding.emojiDownload.setOnClickListener { startDownload(font, binding) }
- binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) }
- binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
- binding.root.setOnClickListener {
- select(font, binding.emojiRadioButton)
- }
- }
-
- private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
- // Switch to downloading style
- binding.emojiDownload.hide()
- binding.emojiCaption.visibility = View.INVISIBLE
- binding.emojiProgress.show()
- binding.emojiProgress.progress = 0
- binding.emojiDownloadCancel.show()
- font.downloadFontFile(context, okHttpClient)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- { progress ->
- // The progress is returned as a float between 0 and 1, or -1 if it could not determined
- if (progress >= 0) {
- binding.emojiProgress.isIndeterminate = false
- val max = binding.emojiProgress.max.toFloat()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- binding.emojiProgress.setProgress((max * progress).toInt(), true)
- } else {
- binding.emojiProgress.progress = (max * progress).toInt()
- }
- } else {
- binding.emojiProgress.isIndeterminate = true
- }
- },
- {
- Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
- updateItem(font, binding)
- },
- {
- finishDownload(font, binding)
- }
- ).also { downloadDisposables[font.id] = it }
- }
-
- private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
- font.deleteDownloadedFile(context)
- downloadDisposables[font.id]?.dispose()
- downloadDisposables[font.id] = null
- updateItem(font, binding)
- }
-
- private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
- select(font, binding.emojiRadioButton)
- updateItem(font, binding)
- // Set the flag to restart the app (because an update has been downloaded)
- if (selected === original && currentNeedsUpdate) {
- updated = true
- currentNeedsUpdate = false
- }
- }
-
- /**
- * Select a font both visually and logically
- *
- * @param font The font to be selected
- * @param radio The radio button associated with it's visual item
- */
- private fun select(font: EmojiCompatFont, radio: RadioButton) {
- selected = font
- radioButtons.forEach { radioButton ->
- radioButton.isChecked = radioButton == radio
- }
- }
-
- /**
- * Called when a "consistent" state is reached, i.e. it's not downloading the font
- *
- * @param font The font to be displayed
- * @param binding The ItemEmojiPrefBinding to show the item in
- */
- private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
- // There's no download going on
- binding.emojiProgress.hide()
- binding.emojiDownloadCancel.hide()
- binding.emojiCaption.show()
- if (font.isDownloaded(context)) {
- // Make it selectable
- binding.emojiDownload.hide()
- binding.emojiRadioButton.show()
- binding.root.isClickable = true
- } else {
- // Make it downloadable
- binding.emojiDownload.show()
- binding.emojiRadioButton.hide()
- binding.root.isClickable = false
- }
-
- // Select it if necessary
- if (font === selected) {
- binding.emojiRadioButton.isChecked = true
- // Update available
- if (!font.isDownloaded(context)) {
- currentNeedsUpdate = true
- }
- } else {
- binding.emojiRadioButton.isChecked = false
- }
- }
-
- private fun saveSelectedFont() {
- val index = selected.id
- Log.i(TAG, "saveSelectedFont: Font ID: $index")
- PreferenceManager
- .getDefaultSharedPreferences(context)
- .edit()
- .putInt(key, index)
- .apply()
- summary = selected.getDisplay(context)
- }
-
- /**
- * User clicked ok -> save the selected font and offer to restart the app if something changed
- */
- private fun onDialogOk() {
- saveSelectedFont()
- if (selected !== original || updated) {
- AlertDialog.Builder(context)
- .setTitle(R.string.restart_required)
- .setMessage(R.string.restart_emoji)
- .setNegativeButton(R.string.later, null)
- .setPositiveButton(R.string.restart) { _, _ ->
- // Restart the app
- // From https://stackoverflow.com/a/17166729/5070653
- val launchIntent = Intent(context, SplashActivity::class.java)
- val mPendingIntent = PendingIntent.getActivity(
- context,
- 0x1f973, // This is the codepoint of the party face emoji :D
- launchIntent,
- NotificationHelper.pendingIntentFlags(false)
- )
- val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
- mgr.set(
- AlarmManager.RTC,
- System.currentTimeMillis() + 100,
- mPendingIntent
- )
- exitProcess(0)
- }.show()
- }
- }
-
- companion object {
- private const val TAG = "EmojiPreference"
- }
-}
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 4d8ba84f..6fdc1e8a 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,28 @@ 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
+ }
+ }
+
+ switchPreference {
+ setTitle(R.string.pref_title_notification_filter_updates)
+ key = PrefKeys.NOTIFICATION_FILTER_UPDATES
+ isIconSpaceReserved = false
+ isChecked = activeAccount.notificationsUpdates
+ setOnPreferenceChangeListener { _, newValue ->
+ updateAccount { it.notificationsUpdates = newValue as Boolean }
+ true
+ }
+ }
}
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
index 61f86627..61d828c1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
@@ -38,14 +38,11 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
-import okhttp3.OkHttpClient
+import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
import javax.inject.Inject
class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
- @Inject
- lateinit var okhttpclient: OkHttpClient
-
@Inject
lateinit var accountManager: AccountManager
@@ -65,11 +62,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
}
- emojiPreference(okhttpclient) {
- setDefaultValue("system_default")
- setIcon(R.drawable.ic_emoji_24dp)
- key = PrefKeys.EMOJI
- setSummary(R.string.system_default)
+ emojiPreference(requireActivity()) {
setTitle(R.string.emoji_style)
icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied)
}
@@ -300,6 +293,12 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
+ override fun onDisplayPreferenceDialog(preference: Preference) {
+ if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) {
+ super.onDisplayPreferenceDialog(preference)
+ }
+ }
+
companion object {
fun newInstance(): PreferencesFragment {
return PreferencesFragment()
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 f8991282..9f99da53 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 1b3b0de6..82dbf163 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
@@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
+import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.StatusViewHelper
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
@@ -37,6 +38,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 +47,22 @@ 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 absoluteTimeFormatter = AbsoluteTimeFormatter()
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 +70,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)
+ if (viewdata.spoilerText.isBlank()) {
+ setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
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)
+ viewState.setContentShow(viewdata.id, !contentShown)
+ setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
setContentWarningButtonText(!contentShown)
}
}
- setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler)
+ setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
}
}
}
@@ -152,7 +156,7 @@ class StatusViewHolder(
private fun setCreatedAt(createdAt: Date?) {
if (statusDisplayOptions.useAbsoluteTime) {
- binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
+ binding.timestampInfo.text = absoluteTimeFormatter.format(createdAt)
} else {
binding.timestampInfo.text = if (createdAt != null) {
val then = createdAt.time
@@ -169,8 +173,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()
}
}
@@ -189,5 +193,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 76ed2ebe..314513eb 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 cd3e5ac0..766ed44a 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/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt
index d833b432..8ca7248c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt
@@ -84,6 +84,10 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
return true
}
+ override fun finish() {
+ super.finishWithoutSlideOutAnimation()
+ }
+
private fun getPageTitle(position: Int): CharSequence {
return when (position) {
0 -> getString(R.string.title_posts)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
index 2d86f5f2..97e4f617 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
@@ -111,9 +111,13 @@ abstract class SearchFragment :
}
}
- override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
+ override fun onViewAccount(id: String) {
+ bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
+ }
- override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
+ override fun onViewTag(tag: String) {
+ bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
+ }
override fun onViewUrl(url: String) {
bottomSheetActivity?.viewUrl(url)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
index 23ff1b07..2e7849c1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
@@ -97,7 +97,7 @@ class SearchStatusesFragment : SearchFragment(), Status
}
override fun onReply(position: Int) {
- searchAdapter.peek(position)?.status?.let { status ->
+ searchAdapter.peek(position)?.let { status ->
reply(status)
}
}
@@ -199,8 +199,8 @@ class SearchStatusesFragment : SearchFragment(), Status
fun newInstance() = SearchStatusesFragment()
}
- private fun reply(status: Status) {
- val actionableStatus = status.actionableStatus
+ private fun reply(status: StatusViewData.Concrete) {
+ val actionableStatus = status.actionable
val mentionedUsernames = actionableStatus.mentions.map { it.username }
.toMutableSet()
.apply {
@@ -216,10 +216,10 @@ class SearchStatusesFragment : SearchFragment(), Status
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername,
- replyingStatusContent = actionableStatus.content.toString()
+ replyingStatusContent = status.content.toString()
)
)
- startActivity(intent)
+ bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
}
private fun more(status: Status, view: View, position: Int) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt
index f9175052..54183888 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt
@@ -172,7 +172,7 @@ class TimelineFragment :
setupRecyclerView()
adapter.addLoadStateListener { loadState ->
- if (loadState.refresh != LoadState.Loading) {
+ if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
binding.swipeRefreshLayout.isRefreshing = false
}
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 252b9880..12422a95 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,22 +15,18 @@
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
import com.keylesspalace.tusky.db.TimelineStatusEntity
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.keylesspalace.tusky.entity.Attachment
+import com.keylesspalace.tusky.entity.Card
import com.keylesspalace.tusky.entity.Emoji
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
@@ -101,7 +97,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
expanded = loading,
contentCollapsed = false,
contentShowing = false,
- pinned = false
+ pinned = false,
+ card = null,
)
}
@@ -119,7 +116,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,
@@ -141,7 +138,8 @@ fun Status.toEntity(
expanded = expanded,
contentShowing = contentShowing,
contentCollapsed = contentCollapsed,
- pinned = actionableStatus.pinned == true
+ pinned = actionableStatus.pinned == true,
+ card = actionableStatus.card?.let(gson::toJson),
)
}
@@ -156,6 +154,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
val application = gson.fromJson(status.application, Status.Application::class.java)
val emojis: List = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
+ val card: Card? = gson.fromJson(status.card, Card::class.java)
val reblog = status.reblogServerId?.let { id ->
Status(
@@ -165,8 +164,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,
@@ -184,7 +182,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
pinned = false,
muted = status.muted,
poll = poll,
- card = null
+ card = card,
)
}
val status = if (reblog != null) {
@@ -195,7 +193,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,
@@ -223,8 +221,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,
@@ -242,14 +239,13 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
pinned = status.pinned,
muted = status.muted,
poll = poll,
- card = null
+ card = card,
)
}
return StatusViewData.Concrete(
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 304b4e5a..7158a7b3 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
@@ -42,7 +42,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
@@ -79,15 +82,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 f70fdcc8..ca7988bb 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
@@ -79,10 +82,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 0c25cbbc..400eb073 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,8 @@ data class AccountEntity(
var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true,
var notificationsSubscriptions: Boolean = true,
+ var notificationsSignUps: Boolean = true,
+ var notificationsUpdates: 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 159a6f52..d5f023e5 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 = 35)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@@ -483,4 +483,62 @@ 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`))");
+ }
+ };
+
+ public static final Migration MIGRATION_33_34 = new Migration(33, 34) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsUpdates` INTEGER NOT NULL DEFAULT 1");
+ }
+ };
+
+ public static final Migration MIGRATION_34_35 = new Migration(34, 35) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT");
+ }
+ };
}
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 393a2392..fe093bd0 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 c9daec0a..34ff6474 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
@@ -31,10 +28,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
@@ -140,22 +135,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/db/InstanceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt
index 52fc3aa8..9b190bc7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt
@@ -19,13 +19,19 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
-import io.reactivex.rxjava3.core.Single
@Dao
interface InstanceDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insertOrReplace(instance: InstanceEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
+ suspend fun insertOrReplace(instance: InstanceInfoEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
+ suspend fun insertOrReplace(emojis: EmojisEntity)
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
- fun loadMetadataForInstance(instance: String): Single
+ suspend fun getInstanceInfo(instance: String): InstanceInfoEntity?
+
+ @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
+ suspend fun getEmojiInfo(instance: String): EmojisEntity?
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt
index dd8e85d0..01767f32 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt
@@ -23,7 +23,7 @@ import com.keylesspalace.tusky.entity.Emoji
@Entity
@TypeConverters(Converters::class)
data class InstanceEntity(
- @field:PrimaryKey var instance: String,
+ @PrimaryKey val instance: String,
val emojiList: List?,
val maximumTootCharacters: Int?,
val maxPollOptions: Int?,
@@ -33,3 +33,20 @@ data class InstanceEntity(
val charactersReservedPerUrl: Int?,
val version: String?
)
+
+@TypeConverters(Converters::class)
+data class EmojisEntity(
+ @PrimaryKey val instance: String,
+ val emojiList: List?
+)
+
+data class InstanceInfoEntity(
+ @PrimaryKey val instance: String,
+ val maximumTootCharacters: Int?,
+ val maxPollOptions: Int?,
+ val maxPollOptionLength: Int?,
+ val minPollDuration: Int?,
+ val maxPollDuration: Int?,
+ val charactersReservedPerUrl: Int?,
+ val version: String?
+)
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
index dd59f2a3..2c6ef188 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
@@ -36,7 +36,7 @@ SELECT s.serverId, s.url, s.timelineUserId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
-s.content, s.attachments, s.poll, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
+s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
index 41b122c3..2c4d45c3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
@@ -78,7 +78,8 @@ data class TimelineStatusEntity(
val expanded: Boolean, // used as the "loading" attribute when this TimelineStatusEntity is a placeholder
val contentCollapsed: Boolean,
val contentShowing: Boolean,
- val pinned: Boolean
+ val pinned: Boolean,
+ val card: String?,
)
@Entity(
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 b0f28261..0861e9cf 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -62,7 +62,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, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
)
.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 7bda6ef7..90dd3026 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
@@ -18,14 +18,13 @@ 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.MediaUploadApi
import com.keylesspalace.tusky.util.getNonNullString
import dagger.Module
import dagger.Provides
@@ -51,11 +50,7 @@ class NetworkModule {
@Provides
@Singleton
- fun providesGson(): Gson {
- return GsonBuilder()
- .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
- .create()
- }
+ fun providesGson() = Gson()
@Provides
@Singleton
@@ -111,10 +106,25 @@ class NetworkModule {
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
+ .addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
.build()
}
@Provides
@Singleton
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create()
+
+ @Provides
+ @Singleton
+ fun providesMediaUploadApi(retrofit: Retrofit, okHttpClient: OkHttpClient): MediaUploadApi {
+ val longTimeOutOkHttpClient = okHttpClient.newBuilder()
+ .readTimeout(100, TimeUnit.SECONDS)
+ .writeTimeout(100, TimeUnit.SECONDS)
+ .build()
+
+ return retrofit.newBuilder()
+ .client(longTimeOutOkHttpClient)
+ .build()
+ .create()
+ }
}
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 672bd5aa..bf5431ee 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") val username: String,
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
- val note: Spanned,
+ val note: String,
val url: String,
val avatar: String,
val header: String,
@@ -46,56 +45,6 @@ data class Account(
} else displayName
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(
@@ -107,7 +56,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 400e9764..00d5659d 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 52011f3d..29fe7f8e 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/IdentityProof.kt b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt
deleted file mode 100644
index 98af734b..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.keylesspalace.tusky.entity
-
-import com.google.gson.annotations.SerializedName
-
-data class IdentityProof(
- val provider: String,
- @SerializedName("provider_username") val username: String,
- @SerializedName("profile_url") val profileUrl: String
-)
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 ae2d74a9..f6e38150 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,10 @@ data class Notification(
FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"),
POLL("poll"),
- STATUS("status");
+ STATUS("status"),
+ SIGN_UP("admin.sign_up"),
+ UPDATE("update"),
+ ;
companion object {
@@ -49,7 +52,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, UPDATE)
}
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 f75ce4e7..19cb7aa6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
@@ -16,9 +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 com.keylesspalace.tusky.util.parseAsMastodonHtml
import java.util.ArrayList
import java.util.Date
@@ -29,7 +29,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") val createdAt: Date,
val emojis: List,
@SerializedName("reblogs_count") val reblogsCount: Int,
@@ -134,8 +134,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) {
@@ -149,71 +150,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 e9581e24..56291a21 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;
@@ -111,10 +115,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,
@@ -707,6 +707,10 @@ 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);
+ case UPDATE:
+ return getString(R.string.notification_update_name);
default:
return "Unknown";
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index b1a47ad8..ad81abe3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -15,6 +15,8 @@
package com.keylesspalace.tusky.fragment;
+import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
+
import android.Manifest;
import android.app.DownloadManager;
import android.content.ClipData;
@@ -56,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.util.LinkHelper;
+import com.keylesspalace.tusky.util.StatusParsingHelper;
import com.keylesspalace.tusky.view.MuteAccountDialog;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
@@ -150,7 +153,7 @@ public abstract class SFragment extends Fragment implements Injectable {
composeOptions.setContentWarning(contentWarning);
composeOptions.setMentionedUsernames(mentionedUsernames);
composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername());
- composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString());
+ composeOptions.setReplyingStatusContent(parseAsMastodonHtml(actionableStatus.getContent()).toString());
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
getActivity().startActivity(intent);
@@ -226,7 +229,7 @@ public abstract class SFragment extends Fragment implements Injectable {
String stringToShare = statusToShare.getAccount().getUsername() +
" - " +
- statusToShare.getContent().toString();
+ StatusParsingHelper.parseAsMastodonHtml(statusToShare.getContent()).toString();
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
sendIntent.setType("text/plain");
diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
index 116e582c..ec37680c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
+++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
@@ -38,7 +38,7 @@ public interface StatusActionListener extends LinkListener {
void onOpenReblog(int position);
void onExpandedChange(boolean expanded, int position);
void onContentHiddenChange(boolean isShowing, int position);
- void onLoadMore(int position);
+ void onLoadMore(int position);
/**
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
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 60af6134..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/* Copyright 2020 Tusky Contributors
- *
- * This file is a part of Tusky.
- *
- * This program is free software; you can redistribute it and/or modify it under the terms of the
- * GNU General Public License as published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
- * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
- * Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Tusky; if not,
- * see . */
-
-package com.keylesspalace.tusky.json
-
-import android.text.Spanned
-import android.text.SpannedString
-import androidx.core.text.HtmlCompat
-import androidx.core.text.parseAsHtml
-import androidx.core.text.toHtml
-import com.google.gson.JsonDeserializationContext
-import com.google.gson.JsonDeserializer
-import com.google.gson.JsonElement
-import com.google.gson.JsonParseException
-import com.google.gson.JsonPrimitive
-import com.google.gson.JsonSerializationContext
-import com.google.gson.JsonSerializer
-import com.keylesspalace.tusky.util.trimTrailingWhitespace
-import java.lang.reflect.Type
-
-class SpannedTypeAdapter : JsonDeserializer, 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(" ", " ")
- ?.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 28d83eca..7357293b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -24,7 +24,6 @@ import com.keylesspalace.tusky.entity.Conversation
import com.keylesspalace.tusky.entity.DeletedStatus
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Filter
-import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.MastoList
@@ -77,10 +76,10 @@ interface MastodonApi {
fun getLists(): Single>
@GET("/api/v1/custom_emojis")
- fun getCustomEmojis(): Single>
+ suspend fun getCustomEmojis(): Result>
@GET("api/v1/instance")
- fun getInstance(): Single
+ suspend fun getInstance(): Result
@GET("api/v1/filters")
fun getFilters(): Single>
@@ -143,27 +142,25 @@ interface MastodonApi {
@POST("api/v1/notifications/clear")
fun clearNotifications(): Single
- @Multipart
- @POST("api/v2/media")
- fun uploadMedia(
- @Part file: MultipartBody.Part,
- @Part description: MultipartBody.Part? = null
- ): Single
-
@FormUrlEncoded
@PUT("api/v1/media/{mediaId}")
- fun updateMedia(
+ suspend fun updateMedia(
@Path("mediaId") mediaId: String,
@Field("description") description: String
- ): Single
+ ): Result
+
+ @GET("api/v1/media/{mediaId}")
+ suspend fun getMedia(
+ @Path("mediaId") mediaId: String
+ ): Response
@POST("api/v1/statuses")
- fun createStatus(
+ suspend fun createStatus(
@Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String,
@Header("Idempotency-Key") idempotencyKey: String,
@Body status: NewStatus
- ): Call
+ ): Result
@GET("api/v1/statuses/{id}")
fun status(
@@ -249,12 +246,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 +262,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 +276,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(
@@ -367,11 +364,6 @@ interface MastodonApi {
@Query("id[]") accountIds: List
): Single>
- @GET("api/v1/accounts/{id}/identity_proofs")
- fun identityProofs(
- @Path("id") accountId: String
- ): Single>
-
@POST("api/v1/pleroma/accounts/{id}/subscribe")
fun subscribeAccount(
@Path("id") accountId: String
@@ -447,7 +439,7 @@ interface MastodonApi {
@Field("redirect_uris") redirectUris: String,
@Field("scopes") scopes: String,
@Field("website") website: String
- ): AppCredentials
+ ): Result
@FormUrlEncoded
@POST("oauth/token")
@@ -458,7 +450,7 @@ interface MastodonApi {
@Field("redirect_uri") redirectUri: String,
@Field("code") code: String,
@Field("grant_type") grantType: String
- ): AccessToken
+ ): Result
@FormUrlEncoded
@POST("api/v1/lists")
@@ -544,26 +536,26 @@ interface MastodonApi {
): Single
@GET("api/v1/announcements")
- fun listAnnouncements(
+ suspend fun listAnnouncements(
@Query("with_dismissed") withDismissed: Boolean = true
- ): Single>
+ ): Result>
@POST("api/v1/announcements/{id}/dismiss")
- fun dismissAnnouncement(
+ suspend fun dismissAnnouncement(
@Path("id") announcementId: String
- ): Single
+ ): Result
@PUT("api/v1/announcements/{id}/reactions/{name}")
- fun addAnnouncementReaction(
+ suspend fun addAnnouncementReaction(
@Path("id") announcementId: String,
@Path("name") name: String
- ): Single
+ ): Result
@DELETE("api/v1/announcements/{id}/reactions/{name}")
- fun removeAnnouncementReaction(
+ suspend fun removeAnnouncementReaction(
@Path("id") announcementId: String,
@Path("name") name: String
- ): Single
+ ): Result
@FormUrlEncoded
@POST("api/v1/reports")
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt
new file mode 100644
index 00000000..c7e9633f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt
@@ -0,0 +1,19 @@
+package com.keylesspalace.tusky.network
+
+import com.keylesspalace.tusky.entity.MediaUploadResult
+import okhttp3.MultipartBody
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+
+/** endpoints defined in this interface will be called with a higher timeout than usual
+ * which is necessary for media uploads to succeed on some servers
+ */
+interface MediaUploadApi {
+ @Multipart
+ @POST("api/v2/media")
+ suspend fun uploadMedia(
+ @Part file: MultipartBody.Part,
+ @Part description: MultipartBody.Part? = null
+ ): Result
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
index 6101dd84..14f82e8b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
@@ -100,7 +100,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
accountId = account.id,
draftId = -1,
idempotencyKey = randomAlphanumericString(16),
- retries = 0
+ retries = 0,
+ mediaProcessed = mutableListOf()
)
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
index a2709f97..e50f4f4f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
@@ -11,6 +11,7 @@ import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
+import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
@@ -29,13 +30,12 @@ import com.keylesspalace.tusky.network.MastodonApi
import dagger.android.AndroidInjection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
+import retrofit2.HttpException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -55,7 +55,7 @@ class SendStatusService : Service(), Injectable {
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
private val statusesToSend = ConcurrentHashMap()
- private val sendCalls = ConcurrentHashMap>()
+ private val sendJobs = ConcurrentHashMap()
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
@@ -64,12 +64,9 @@ class SendStatusService : Service(), Injectable {
super.onCreate()
}
- override fun onBind(intent: Intent): IBinder? {
- return null
- }
+ override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
-
if (intent.hasExtra(KEY_STATUS)) {
val statusToSend = intent.getParcelableExtra(KEY_STATUS)
?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra")
@@ -129,82 +126,94 @@ class SendStatusService : Service(), Injectable {
statusToSend.retries++
- val newStatus = NewStatus(
- statusToSend.text,
- statusToSend.warningText,
- statusToSend.inReplyToId,
- statusToSend.visibility,
- statusToSend.sensitive,
- statusToSend.mediaIds,
- statusToSend.scheduledAt,
- statusToSend.poll
- )
+ sendJobs[statusId] = serviceScope.launch {
+ try {
+ var mediaCheckRetries = 0
+ while (statusToSend.mediaProcessed.any { !it }) {
+ delay(1000L * mediaCheckRetries)
+ statusToSend.mediaProcessed.forEachIndexed { index, processed ->
+ if (!processed) {
+ // Mastodon returns 206 if the media was not yet processed
+ statusToSend.mediaProcessed[index] = mastodonApi.getMedia(statusToSend.mediaIds[index]).code() == 200
+ }
+ }
+ mediaCheckRetries ++
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "failed getting media status", e)
+ retrySending(statusId)
+ return@launch
+ }
- val sendCall = mastodonApi.createStatus(
- "Bearer " + account.accessToken,
- account.domain,
- statusToSend.idempotencyKey,
- newStatus
- )
+ val newStatus = NewStatus(
+ statusToSend.text,
+ statusToSend.warningText,
+ statusToSend.inReplyToId,
+ statusToSend.visibility,
+ statusToSend.sensitive,
+ statusToSend.mediaIds,
+ statusToSend.scheduledAt,
+ statusToSend.poll
+ )
- sendCalls[statusId] = sendCall
+ mastodonApi.createStatus(
+ "Bearer " + account.accessToken,
+ account.domain,
+ statusToSend.idempotencyKey,
+ newStatus
+ ).fold({ sentStatus ->
+ statusesToSend.remove(statusId)
+ // If the status was loaded from a draft, delete the draft and associated media files.
+ if (statusToSend.draftId != 0) {
+ draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
+ }
- val callback = object : Callback {
- override fun onResponse(call: Call, response: Response) {
- serviceScope.launch {
+ val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
- val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
+ if (scheduled) {
+ eventHub.dispatch(StatusScheduledEvent(sentStatus))
+ } else {
+ eventHub.dispatch(StatusComposedEvent(sentStatus))
+ }
+
+ notificationManager.cancel(statusId)
+ }, { throwable ->
+ Log.w(TAG, "failed sending status", throwable)
+ if (throwable is HttpException) {
+ // the server refused to accept the status, save status & show error message
statusesToSend.remove(statusId)
+ saveStatusToDrafts(statusToSend)
- if (response.isSuccessful) {
- // If the status was loaded from a draft, delete the draft and associated media files.
- if (statusToSend.draftId != 0) {
- draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
- }
-
- if (scheduled) {
- response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
- } else {
- response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
- }
-
- notificationManager.cancel(statusId)
- } else {
- // the server refused to accept the status, save status & show error message
- saveStatusToDrafts(statusToSend)
-
- val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_notify)
- .setContentTitle(getString(R.string.send_post_notification_error_title))
- .setContentText(getString(R.string.send_post_notification_saved_content))
- .setColor(
- ContextCompat.getColor(
- this@SendStatusService,
- R.color.notification_color
- )
+ val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notify)
+ .setContentTitle(getString(R.string.send_post_notification_error_title))
+ .setContentText(getString(R.string.send_post_notification_saved_content))
+ .setColor(
+ ContextCompat.getColor(
+ this@SendStatusService,
+ R.color.notification_color
)
+ )
- notificationManager.cancel(statusId)
- notificationManager.notify(errorNotificationId--, builder.build())
- }
- stopSelfWhenDone()
+ notificationManager.cancel(statusId)
+ notificationManager.notify(errorNotificationId--, builder.build())
+ } else {
+ // a network problem occurred, let's retry sending the status
+ retrySending(statusId)
}
- }
-
- override fun onFailure(call: Call, t: Throwable) {
- serviceScope.launch {
- var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong())
- if (backoff > MAX_RETRY_INTERVAL) {
- backoff = MAX_RETRY_INTERVAL
- }
-
- delay(backoff)
- sendStatus(statusId)
- }
- }
+ })
+ stopSelfWhenDone()
}
+ }
- sendCall.enqueue(callback)
+ private suspend fun retrySending(statusId: Int) {
+ // when statusToSend == null, sending has been canceled
+ val statusToSend = statusesToSend[statusId] ?: return
+
+ val backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()).coerceAtMost(MAX_RETRY_INTERVAL)
+
+ delay(backoff)
+ sendStatus(statusId)
}
private fun stopSelfWhenDone() {
@@ -218,8 +227,8 @@ class SendStatusService : Service(), Injectable {
private fun cancelSending(statusId: Int) = serviceScope.launch {
val statusToCancel = statusesToSend.remove(statusId)
if (statusToCancel != null) {
- val sendCall = sendCalls.remove(statusId)
- sendCall?.cancel()
+ val sendJob = sendJobs.remove(statusId)
+ sendJob?.cancel()
saveStatusToDrafts(statusToCancel)
@@ -263,6 +272,7 @@ class SendStatusService : Service(), Injectable {
}
companion object {
+ private const val TAG = "SendStatusService"
private const val KEY_STATUS = "status"
private const val KEY_CANCEL = "cancel_id"
@@ -319,5 +329,6 @@ data class StatusToSend(
val accountId: Long,
val draftId: Int,
val idempotencyKey: String,
- var retries: Int
+ var retries: Int,
+ val mediaProcessed: MutableList
) : Parcelable
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 c59ba58b..6540601a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
@@ -59,6 +59,8 @@ 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 NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
index 1569cb15..85270081 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
@@ -1,7 +1,9 @@
package com.keylesspalace.tusky.settings
import android.content.Context
+import androidx.activity.result.ActivityResultRegistryOwner
import androidx.annotation.StringRes
+import androidx.lifecycle.LifecycleOwner
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@@ -10,8 +12,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreference
-import com.keylesspalace.tusky.components.preference.EmojiPreference
-import okhttp3.OkHttpClient
+import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
class PreferenceParent(
val context: Context,
@@ -32,8 +33,9 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
return pref
}
-inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference {
- val pref = EmojiPreference(context, okHttpClient)
+inline fun PreferenceParent.emojiPreference(activity: A, builder: EmojiPickerPreference.() -> Unit): EmojiPickerPreference
+ where A : Context, A : ActivityResultRegistryOwner, A : LifecycleOwner {
+ val pref = EmojiPickerPreference.get(activity)
builder(pref)
addPref(pref)
return pref
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt
new file mode 100644
index 00000000..7d46388b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt
@@ -0,0 +1,59 @@
+/* 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.util
+
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+class AbsoluteTimeFormatter @JvmOverloads constructor(private val tz: TimeZone = TimeZone.getDefault()) {
+ private val sameDaySdf = SimpleDateFormat("HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
+ private val sameYearSdf = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
+ private val otherYearSdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply { this.timeZone = tz }
+ private val otherYearCompleteSdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
+
+ @JvmOverloads
+ fun format(time: Date?, shortFormat: Boolean = true, now: Date = Date()): String {
+ return when {
+ time == null -> "??"
+ isSameDate(time, now, tz) -> sameDaySdf.format(time)
+ isSameYear(time, now, tz) -> sameYearSdf.format(time)
+ shortFormat -> otherYearSdf.format(time)
+ else -> otherYearCompleteSdf.format(time)
+ }
+ }
+
+ companion object {
+
+ private fun isSameDate(dateOne: Date, dateTwo: Date, tz: TimeZone): Boolean {
+ val calendarOne = Calendar.getInstance(tz).apply { time = dateOne }
+ val calendarTwo = Calendar.getInstance(tz).apply { time = dateTwo }
+
+ return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) &&
+ calendarOne.get(Calendar.MONTH) == calendarTwo.get(Calendar.MONTH) &&
+ calendarOne.get(Calendar.DAY_OF_MONTH) == calendarTwo.get(Calendar.DAY_OF_MONTH)
+ }
+
+ private fun isSameYear(dateOne: Date, dateTwo: Date, timeZone1: TimeZone): Boolean {
+ val calendarOne = Calendar.getInstance(timeZone1).apply { time = dateOne }
+ val calendarTwo = Calendar.getInstance(timeZone1).apply { time = dateTwo }
+
+ return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR)
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt
deleted file mode 100644
index 385be6c1..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt
+++ /dev/null
@@ -1,364 +0,0 @@
-package com.keylesspalace.tusky.util
-
-import android.content.Context
-import android.util.Log
-import android.util.Pair
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-import androidx.annotation.VisibleForTesting
-import com.keylesspalace.tusky.R
-import de.c1710.filemojicompat.FileEmojiCompatConfig
-import io.reactivex.rxjava3.core.Observable
-import io.reactivex.rxjava3.core.ObservableEmitter
-import io.reactivex.rxjava3.schedulers.Schedulers
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import okhttp3.ResponseBody
-import okhttp3.internal.toLongOrDefault
-import okio.Source
-import okio.buffer
-import okio.sink
-import java.io.EOFException
-import java.io.File
-import java.io.FilenameFilter
-import java.io.IOException
-import kotlin.math.max
-
-/**
- * This class bundles information about an emoji font as well as many convenient actions.
- */
-class EmojiCompatFont(
- val name: String,
- private val display: String,
- @StringRes val caption: Int,
- @DrawableRes val img: Int,
- val url: String,
- // The version is stored as a String in the x.xx.xx format (to be able to compare versions)
- val version: String
-) {
-
- private val versionCode = getVersionCode(version)
-
- // A list of all available font files and whether they are older than the current version or not
- // They are ordered by their version codes in ascending order
- private var existingFontFileCache: List>>? = null
-
- val id: Int
- get() = FONTS.indexOf(this)
-
- fun getDisplay(context: Context): String {
- return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default)
- }
-
- /**
- * This method will return the actual font file (regardless of its existence) for
- * the current version (not necessarily the latest!).
- *
- * @return The font (TTF) file or null if called on SYSTEM_FONT
- */
- private fun getFontFile(context: Context): File? {
- return if (this !== SYSTEM_DEFAULT) {
- val directory = File(context.getExternalFilesDir(null), DIRECTORY)
- File(directory, "$name$version.ttf")
- } else {
- null
- }
- }
-
- fun getConfig(context: Context): FileEmojiCompatConfig {
- return FileEmojiCompatConfig(context, getLatestFontFile(context))
- }
-
- fun isDownloaded(context: Context): Boolean {
- return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context)
- }
-
- /**
- * Checks whether there is already a font version that satisfies the current version, i.e. it
- * has a higher or equal version code.
- *
- * @param context The Context
- * @return Whether there is a font file with a higher or equal version code to the current
- */
- private fun fontFileExists(context: Context): Boolean {
- val existingFontFiles = getExistingFontFiles(context)
- return if (existingFontFiles.isNotEmpty()) {
- compareVersions(existingFontFiles.last().second, versionCode) >= 0
- } else {
- false
- }
- }
-
- /**
- * Deletes any older version of a font
- *
- * @param context The current Context
- */
- private fun deleteOldVersions(context: Context) {
- val existingFontFiles = getExistingFontFiles(context)
- Log.d(TAG, "deleting old versions...")
- Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size))
- for (fileExists in existingFontFiles) {
- if (compareVersions(fileExists.second, versionCode) < 0) {
- val file = fileExists.first
- // Uses side effects!
- Log.d(
- TAG,
- String.format(
- "Deleted %s successfully: %s", file.absolutePath,
- file.delete()
- )
- )
- }
- }
- }
-
- /**
- * Loads all font files that are inside the files directory into an ArrayList with the information
- * on whether they are older than the currently available version or not.
- *
- * @param context The Context
- */
- private fun getExistingFontFiles(context: Context): List>> {
- // Only load it once
- existingFontFileCache?.let {
- return it
- }
- // If we call this on the system default font, just return nothing...
- if (this === SYSTEM_DEFAULT) {
- existingFontFileCache = emptyList()
- return emptyList()
- }
-
- val directory = File(context.getExternalFilesDir(null), DIRECTORY)
- // It will search for old versions using a regex that matches the font's name plus
- // (if present) a version code. No version code will be regarded as version 0.
- val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern()
- val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") }
- val foundFontFiles = directory.listFiles(ttfFilter).orEmpty()
- Log.d(
- TAG,
- String.format(
- "loadExistingFontFiles: %d other font files found",
- foundFontFiles.size
- )
- )
-
- return foundFontFiles.map { file ->
- val matcher = fontRegex.matcher(file.name)
- val versionCode = if (matcher.matches()) {
- val version = matcher.group(1)
- getVersionCode(version)
- } else {
- listOf(0)
- }
- Pair(file, versionCode)
- }.sortedWith { a, b ->
- compareVersions(a.second, b.second)
- }.also {
- existingFontFileCache = it
- }
- }
-
- /**
- * Returns the current or latest version of this font file (if there is any)
- *
- * @param context The Context
- * @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font.
- */
- private fun getLatestFontFile(context: Context): File? {
- val current = getFontFile(context)
- if (current != null && current.exists()) return current
- val existingFontFiles = getExistingFontFiles(context)
- return existingFontFiles.firstOrNull()?.first
- }
-
- private fun getVersionCode(version: String?): List {
- if (version == null) return listOf(0)
- return version.split(".").map {
- it.toIntOrNull() ?: 0
- }
- }
-
- fun downloadFontFile(
- context: Context,
- okHttpClient: OkHttpClient
- ): Observable {
- return Observable.create { emitter: ObservableEmitter ->
- // It is possible (and very likely) that the file does not exist yet
- val downloadFile = getFontFile(context)!!
- if (!downloadFile.exists()) {
- downloadFile.parentFile?.mkdirs()
- downloadFile.createNewFile()
- }
- val request = Request.Builder().url(url)
- .build()
-
- val sink = downloadFile.sink().buffer()
- var source: Source? = null
- try {
- // Download!
- val response = okHttpClient.newCall(request).execute()
-
- val responseBody = response.body
- if (response.isSuccessful && responseBody != null) {
- val size = response.length()
- var progress = 0f
- source = responseBody.source()
- try {
- while (!emitter.isDisposed) {
- sink.write(source, CHUNK_SIZE)
- progress += CHUNK_SIZE.toFloat()
- if (size > 0) {
- emitter.onNext(progress / size)
- } else {
- emitter.onNext(-1f)
- }
- }
- } catch (ex: EOFException) {
- /*
- This means we've finished downloading the file since sink.write
- will throw an EOFException when the file to be read is empty.
- */
- }
- } else {
- Log.e(TAG, "Downloading $url failed. Status code: ${response.code}")
- emitter.tryOnError(Exception())
- }
- } catch (ex: IOException) {
- Log.e(TAG, "Downloading $url failed.", ex)
- downloadFile.deleteIfExists()
- emitter.tryOnError(ex)
- } finally {
- source?.close()
- sink.close()
- if (emitter.isDisposed) {
- downloadFile.deleteIfExists()
- } else {
- deleteOldVersions(context)
- emitter.onComplete()
- }
- }
- }
- .subscribeOn(Schedulers.io())
- }
-
- /**
- * Deletes the downloaded file, if it exists. Should be called when a download gets cancelled.
- */
- fun deleteDownloadedFile(context: Context) {
- getFontFile(context)?.deleteIfExists()
- }
-
- override fun toString(): String {
- return display
- }
-
- companion object {
- private const val TAG = "EmojiCompatFont"
-
- /**
- * This String represents the sub-directory the fonts are stored in.
- */
- private const val DIRECTORY = "emoji"
-
- private const val CHUNK_SIZE = 4096L
-
- // The system font gets some special behavior...
- val SYSTEM_DEFAULT = EmojiCompatFont(
- "system-default",
- "System Default",
- R.string.caption_systememoji,
- R.drawable.ic_emoji_34dp,
- "",
- "0"
- )
- val BLOBMOJI = EmojiCompatFont(
- "Blobmoji",
- "Blobmoji",
- R.string.caption_blobmoji,
- R.drawable.ic_blobmoji,
- "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
- "14.0.1"
- )
- val TWEMOJI = EmojiCompatFont(
- "Twemoji",
- "Twemoji",
- R.string.caption_twemoji,
- R.drawable.ic_twemoji,
- "https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
- "14.0.0"
- )
- val NOTOEMOJI = EmojiCompatFont(
- "NotoEmoji",
- "Noto Emoji",
- R.string.caption_notoemoji,
- R.drawable.ic_notoemoji,
- "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
- "14.0.0"
- )
-
- /**
- * This array stores all available EmojiCompat fonts.
- * References to them can simply be saved by saving their indices
- */
- val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI)
-
- /**
- * Returns the Emoji font associated with this ID
- *
- * @param id the ID of this font
- * @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range.
- */
- fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT }
-
- /**
- * Compares two version codes to each other
- *
- * @param versionA The first version
- * @param versionB The second version
- * @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise
- */
- @VisibleForTesting
- fun compareVersions(versionA: List, versionB: List): Int {
- val len = max(versionB.size, versionA.size)
- for (i in 0 until len) {
-
- val vA = versionA.getOrElse(i) { 0 }
- val vB = versionB.getOrElse(i) { 0 }
-
- // It needs to be decided on the next level
- if (vA == vB) continue
- // Okay, is version B newer or version A?
- return vA.compareTo(vB)
- }
-
- // The versions are equal
- return 0
- }
-
- /**
- * This method is needed because when transparent compression is used OkHttp reports
- * [ResponseBody.contentLength] as -1. We try to get the header which server sent
- * us manually here.
- *
- * @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259)
- */
- private fun Response.length(): Long {
- networkResponse?.let {
- val header = it.header("Content-Length") ?: return -1
- return header.toLongOrDefault(-1)
- }
-
- // In case it's a fully cached response
- return body?.contentLength() ?: -1
- }
-
- private fun File.deleteIfExists() {
- if (exists() && !delete()) {
- Log.e(TAG, "Could not delete file $this")
- }
- }
- }
-}
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 00000000..2ac4782c
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt
@@ -0,0 +1,63 @@
+/* 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
+
+/**
+ * parse a String containing html from the Mastodon api to Spanned
+ */
+fun String.parseAsMastodonHtml(): Spanned {
+ return this.replace("
", "
")
+ .replace("
", "
")
+ .replace("
", "
")
+ .replace(" ", " ")
+ .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/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
index 60ac73f4..0752c4e5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
@@ -34,20 +34,16 @@ import com.keylesspalace.tusky.viewdata.PollViewData
import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
import java.text.NumberFormat
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
import kotlin.math.min
class StatusViewHelper(private val itemView: View) {
+ private val absoluteTimeFormatter = AbsoluteTimeFormatter()
+
interface MediaPreviewListener {
fun onViewMedia(v: View?, idx: Int)
fun onContentHiddenChange(isShowing: Boolean)
}
- private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
- private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
-
fun setMediasPreview(
statusDisplayOptions: StatusDisplayOptions,
attachments: List,
@@ -295,7 +291,7 @@ class StatusViewHelper(private val itemView: View) {
context.getString(R.string.poll_info_closed)
} else {
if (useAbsoluteTime) {
- context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
+ context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
} else {
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
}
@@ -330,18 +326,6 @@ class StatusViewHelper(private val itemView: View) {
}
}
- fun getAbsoluteTime(time: Date?): String {
- return if (time != null) {
- if (android.text.format.DateUtils.isToday(time.time)) {
- shortSdf.format(time)
- } else {
- longSdf.format(time)
- }
- } else {
- "??:??:??"
- }
- }
-
companion object {
val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter)
val NO_INPUT_FILTER = arrayOfNulls(0)
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java
deleted file mode 100644
index dceef0f3..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Copyright 2019 kyori19
- *
- * 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.util;
-
-import androidx.annotation.NonNull;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class VersionUtils {
-
- private int major;
- private int minor;
- private int patch;
-
- public VersionUtils(@NonNull String versionString) {
- String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
- Pattern pattern = Pattern.compile(regex);
- Matcher matcher = pattern.matcher(versionString);
- if (matcher.find()) {
- major = Integer.parseInt(matcher.group(1));
- minor = Integer.parseInt(matcher.group(2));
- patch = Integer.parseInt(matcher.group(3));
- }
- }
-
- public boolean supportsScheduledToots() {
- return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2);
- }
-
-}
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 52d9713f..fef9c0bb 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 d8f27157..8ac212d9 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
@@ -74,45 +77,17 @@ sealed class StatusViewData {
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 f3539f8d..17aa38c7 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/res/drawable/bot_badge.xml b/app/src/main/res/drawable/bot_badge.xml
new file mode 100644
index 00000000..6f857df5
--- /dev/null
+++ b/app/src/main/res/drawable/bot_badge.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_briefcase.xml b/app/src/main/res/drawable/ic_briefcase.xml
index eeb80619..6df5b88b 100644
--- a/app/src/main/res/drawable/ic_briefcase.xml
+++ b/app/src/main/res/drawable/ic_briefcase.xml
@@ -4,6 +4,6 @@
android:viewportHeight="24"
android:viewportWidth="24">
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml
new file mode 100644
index 00000000..2844bafe
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml
index 78865410..28e12cad 100644
--- a/app/src/main/res/layout/activity_account.xml
+++ b/app/src/main/res/layout/activity_account.xml
@@ -112,7 +112,7 @@
app:layout_constraintStart_toStartOf="@id/guideAvatar"
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
-
-
-
-
-
+ tools:visibility="visible">
-
+
-
+
-
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/accountMovedView">
+ app:layout_constraintTop_toBottomOf="@id/accountMovedView">
+ app:layout_constraintTop_toBottomOf="@id/accountMovedView">
-
-
+ android:layout_height="wrap_content" />
+
+
diff --git a/app/src/main/res/layout/dialog_emojicompat.xml b/app/src/main/res/layout/dialog_emojicompat.xml
deleted file mode 100644
index 6850e045..00000000
--- a/app/src/main/res/layout/dialog_emojicompat.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml
index a7b3a0ef..c1565e09 100644
--- a/app/src/main/res/layout/item_account.xml
+++ b/app/src/main/res/layout/item_account.xml
@@ -32,7 +32,7 @@
tools:src="#000"
tools:visibility="visible" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- تم الاحتفاظ بنسخة مِن التبويق في مسوداتك
حرر
لا يحتوي مثيل خادومكم %s على أية حزمة إيموجي مخصصة
- تم نسخه إلى الحافظة
نوع الإيموجي
الإفتراضي في النظام
يجب عليك أولا تنزيل حزمة الإيموجي هذه
@@ -552,4 +551,10 @@
180 يومًا
365 يومًا
تحرير منشور
+ حسابات جديدة
+ لِج
+ قام %s بإنشاء حساب
+ أحدهم أنشأ حسابا جديدا
+ منشورات تم تعديلها
+ قام %s بتعديل منشوره
\ No newline at end of file
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index a6ca7a65..092db218 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -123,7 +123,6 @@
Извършва се търсене…
По подразбиране от системата
Стил на емоджи
- Копирано в клипборда
Инстанцията ви %s няма персонализирани емоджита
Композиране
Копие от публикацията е запазено във вашите чернови
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index 79ecc214..8354954e 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -48,7 +48,6 @@
আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে
সিস্টেমের ডিফল্ট
ইমোজি স্টাইল
- ক্লিপবোর্ডে অনুলিপি করা হয়েছে
আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই
রচনা
টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে
diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml
index 92390c48..1233cc2b 100644
--- a/app/src/main/res/values-bn-rIN/strings.xml
+++ b/app/src/main/res/values-bn-rIN/strings.xml
@@ -304,7 +304,6 @@
টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে
রচনা
আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই
- ক্লিপবোর্ডে অনুলিপি করা হয়েছে
ইমোজি স্টাইল
সিস্টেমের ডিফল্ট
আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 27fa8122..40c74c9b 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -20,8 +20,8 @@
Notificacions
Local
Federació
- Toot
- Posts
+ Fil
+ Publicacions
Seguits
Seguidors
Preferits
@@ -31,19 +31,19 @@
Edita el perfil
Esborranys
\@%s
- %s tootejat
+ %s ha impulsat
Contingut sensible
Fes clic per a visualitzar-lo
Mostra\'n més
Mostra\'n menys
No hi res aquí. Llisca avall per a actualitzar!
- %s ha impulsat el teu toot
- %s ha marcat com a preferit el teu toot
+ %s ha impulsat la teva publicació
+ %s ha marcat com a preferida la teva publicació
%s et segueix
Denuncia @%s
Cap comentari addicional?
Respon
- Retooteja
+ Impulsa
Preferit
Més
Escriure
@@ -55,8 +55,8 @@
Deixa de blocar
Denuncia
Elimina
- TOOT
- TOOT!
+ PUBLICA
+ PUBLICA!
Torna a intentar-ho
Tanca
Perfil
@@ -83,8 +83,8 @@
Esborranys
S\'està baixant %1$s
Copia l\'enllaç
- Comparteix l\'URL del toot a…
- Comparteix el toot a…
+ Comparteix l\'URL de la publicació a…
+ Comparteix la publicació a…
Enviat!
Usuari desblocat
Usuari sense silenciar
@@ -132,10 +132,10 @@
Amaga el botó de redacció en desplaçament
Filtre de la cronologia
Pestanyes
- Mostra els retoots
+ Mostra els impulsos
Mostra les respostes
Mostra les previsualitzacions
- Privacitat predeterminada dels toots
+ Privacitat per defecte de les publicacions
Publicació
Pública
Sense llistar
@@ -145,7 +145,7 @@
Notificacions sobre mencions noves
Seguidors nous
Notificacions sobre nous seguidors
- Retoots
+ Impulsos
Notificacions si retootejents els teus toots
Preferits
Notificacions si marquen com a preferits els teus toots
@@ -172,8 +172,8 @@
https://git.chinwag.org/chinwag/chinwag-android/issues
Perfil del Tusky
- Comparteix el contingut del toot
- Comparteix l\'enllaç al toot
+ Comparteix el contingut de la publicació
+ Comparteix l\'enllaç a la publicació
Imatges
Vídeo
@@ -193,7 +193,7 @@
En resposta a @%s
carrega\'n més
Vota
- S\'ha produït un error en enviar el tut.
+ S\'ha produït un error en publicar.
Pestanyes
Llicències
Amplia
@@ -209,11 +209,11 @@
Multimèdia amagada
Amaga
Estàs segur de tancar la sessió de %1$s\?
- Amaga els retoots
+ Amaga els impulsos
Mostra els impulsos
Elimina i reecririu
Obre el menú
- Visibilitat del toot
+ Visibilitat de la publicació
Contingut sensible
Afegir una pestanya
Enllaços
@@ -227,17 +227,17 @@
Baixa el fitxer
Compartir la imatge a …
Enviat!
- S\'ha enviat la petició de seguiment
+ Petició enviada
Amb respostes
Teclat d\'emojis
Obrir el media #%d
- Obrir com %s
+ Obre com a %s
S\'està Descarregant media
Resposta enviada correctament.
Resposta …
Revocar la petició de seguiment\?
Vols eliminar aquest toot\?
- Esborrar i reescriure aquest toot\?
+ Vols eliminar i reescriure aquesta publicació\?
Finalització de les enquetes
Tema
Cronologia
@@ -267,7 +267,7 @@
Eliminar
Afegir un compte
Obre l\'autor de l\'impuls
- Mostra els retoots
+ Mostra els impulsos
Notificacions d\'enquestes que han finalitzat
Línia de temps públiques
Actualització
@@ -297,20 +297,19 @@
Protegir el compte
S\'haurà d\'admetre els seguidors manualment
Guardar l\'esborrany\?
- Enviant toot…
- Error enviant el toot
- Enviant toots
+ S\'està publicant…
+ Error en publicar
+ S\'esatan enviant les publicacions
Envio anul·lat
- Una copia del toot s\'ha guardat a esborranys
+ S\'ha guardat una còpia de la publicació als esborranys
Escriure
La teva instància %s no te emojis personalitzats
- Copia al porta papers
Estil dels emojis
Sistema per defecte
Hauràs de descarregar el joc d\'emojis
Cercant…
Expandir/ocultar tots els estats
- Obrir toot
+ Obre la publicació
Cal reiniciar l\'aplicació
Has de reiniciar l\'aplicació per tal d\'aplicar aquests canvis
Més tard
@@ -361,7 +360,7 @@
Netejar
Filtrar
Aplicar
- Escriure un toot
+ Escriure una publicació
Escriure
Mostra l\'indicador dels bots
Vols netejar totes les notificacions permanentment\?
@@ -375,8 +374,8 @@
L\'enquesta on has votat està tancada
La enquesta que heu creat ha finalitzat
Advertència: %s
- Toot fixat
- Toot no fixat
+ Fixat
+ No fixis
Fixar
Respost
Accions per a la imatge %s
@@ -386,7 +385,7 @@
Silenciar %s
%s visible
Amagar el domini sencer
- Obrir sempre els toots marcats amb contingut sensible
+ Mostra sempre obertes les publicacions marcades amb avisos de contingut
Paraula sencera
Ventall actual d\'emojis de Google
Enquesta amb opcions: %1$s, %2$s, %3$s, %4$s; %5$s
@@ -419,12 +418,12 @@
Múltiples tries
Tria %d
Preferits
- Toots programats
+ Publicacions programades
Preferit
Edita
Preferits
- Toots programats
- Programar el toot
+ Publicacions programades
+ Programa la publicació
Reiniciar
Desenvolupat per Tusky
S\'ha afegit a les adreces d\'interès
@@ -450,7 +449,7 @@
Silenciar @%s\?
Bloquejar @%s\?
No silenciar la conversació
- Conversació muda
+ Silencia la conversa
%s ha sol·licitat seguir-te
A baix
A dalt
@@ -488,15 +487,15 @@
Adjuncions
Àudio
Notificacions quan algú a qui esteu subscrit publica un tut nou
- Tuts nous
+ Publicacions noves
emojis personalitzats animats
algú a qui estic subscrit acaba de publicar un tut nou
%s acaba de fer una publicació
Avisos
- S\'ha esborrat el tut del qual en vau fer un esborrany de resposta
+ S\'ha eliminat la publicació a la qual vau fer un esborrany de resposta
S\'ha eliminat l\'esborrany
No s\'ha pogut carregar la informació de la resposta
- No s\'ha pogut enviar aquest tut!
+ No s\'ha pogut publicar!
Segur que voleu esborrar la llista %s\?
- No podeu pujar més de %1$d adjunts multimèdia.
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index 8dd86758..ba275b0b 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -23,7 +23,7 @@
هاشتاگی
پیشاندانی دڵخوازەکان
پیشاندانی بەهێزکردنەکان
- کردنەوەی بەهێزکردنی نووسەر
+ پۆستکەرەوەکە ببینە
هاشتاگ
ئاماژەکان
بەستەرەکان
@@ -57,14 +57,14 @@
وێنە بگرە
زیادکردنی ڕاپرسی
زیادکردنی میدیا
- کردنەوە لە وێبگەڕ
+ لە وێبگەڕ بیکەوە
میدیا
بەدواداچونی داواکاریەکان بکە
دۆمەینە شاراوەکان
بەکارهێنەرە بلۆککراوەکان
بەکارهێنەرە گۆڕاوەکان
نیشانەکان
- دڵخوازەکان
+ بەدڵبوونەکان
پەسەندکراوەکانی ئەژمێر
پەسەندەکان
پرۆفایل
@@ -76,12 +76,12 @@
سڕینەوە
دەستکاری
گوزارشەکان
- پیشاندانی بەهێزکردنەکان
+ پۆستکردنەوەکان نیشان بدە
شاردنەوەی بەهێزکردنەکان
بەربەست کردن لاببە
بلۆک
بەدوادانەچو
- بەدواداکەوتن
+ شوێنی بکەوە
ئایا دڵنیایت لەوەی دەتەوێت بچیتەدەرەوە لە هەژماری %1$s؟
چوونەدەرەوە
چوونەژوورەوە لەگەڵ ماستۆدۆن
@@ -90,7 +90,7 @@
لابردنی دڵخوازەکان
نیشانه
دڵخواز
- لابردنی بەهێزکردن
+ پۆستکردنەوەکە بگەڕێنەوە
بەهێزکردن
وەڵام
وەڵامدانەوەی خێرا
@@ -110,7 +110,7 @@
کرتە بکە بۆ بینین
میدیا شاراوە
ناوەڕۆکی هەستیار
- %s بەرزکرا
+ %s پۆستی کردەوە
\@%s
مۆڵەتەکان
ڕاگه یه نراوەکان
@@ -122,12 +122,12 @@
بەکارهێنەرە بێدەنگ
نیشانەکان
دڵخوازەکان
- شوێنکەوتوان
- بەدوادا
+ شوێنکەوتوو
+ شوێنکەوتنەکان
چەسپا
لەگەڵ وەڵامەکان
- بابەتەکان
- توت
+ پۆست
+ زنجیرە
سەرخشتەکان
نامە ڕاستەوخۆکان
گشتی
@@ -140,19 +140,19 @@
مۆڵەت بۆ پاشکەوتکردنی میدیا پێویستە.
مۆڵەت بۆ خوێندنەوەی میدیا پێویستە.
ئەم فایلە ناتوانرێت بکرێتەوە.
- ناتوانرێت ئەو جۆرە فایلە باربکرێت.
- فایلەکانی دەنگ دەبێت کەمتر بێت لە ٤٠MB.
- پێویستە فایلەکانی ڤیدیۆ کەمتر لە 40 مێگابایت بن.
- فایلەکە دەبێت کەمتر بێت لە 8 مێگابایت.
- ڕەستە زۆر درێژە!
+ ناتوانیت لەم جۆرە فایلانە بەرز بکەیتەوە.
+ دەبێت فایلە دەنگییەکان لە 40 مێگابایت گەورەتر نەبن.
+ دەبێت ڤیدیۆکان لە 40 مێگابایت گەورەتر نەبن.
+ فایلەکە دەبێت لە 8 مێگابایت بچووکتر بێت.
+ ئەم نووسینە زۆر درێژە!
سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە.
ڕێپێدان ڕەتکرایەوە.
هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا.
نەیتوانی وێبگەڕبدۆزێتەوە بۆ بەکارهێنان.
سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە.
- دۆمەینی نادروست تێنووسکرا
- ئەمە ناتوانێت بەتاڵ بێت.
- هەڵەیەک لە تۆڕ ڕوویدا! تکایە پەیوەندیت بپشکنە و دوبارە هەوڵ بدە!
+ دۆمەینێکی نادروستت نووسیوە
+ ناکرێت ئەمە بەتاڵ بێت.
+ هەڵەیەک لە پەیوەندییەکەدا ڕوویدا. تکایە دڵنیا ببەوە لە بەردەستبوونی هێڵی ئینتەرنێت.
هەڵەیەک ڕوویدا.
تایبەتمەندی بابەت گریمانەیی
دەرگای پرۆکسی HTTP
@@ -249,7 +249,7 @@
\n
\nکارتێکردنی ئاگانامەکانی پاڵپێوەنان، بەڵام دەتوانیت بە پەسەندکردنە ئاگانامەکانت دا بخشێنیەوە بە دەستی.
ڕزگارکرا
- تێبینی تایبەتی تۆ دەربارەی ئەم ئەژمێرە
+ تێبینیی تایبەتیت بۆ ئەم هەژمارە
Wellbeing
شاردنەوەی ناونیشانی شریتی ئامڕازی سەرەوە
پیشاندانی دیالۆگی دووپاتکردنەوە پێش بەهێزکردن
@@ -294,7 +294,7 @@
ڕاپرسییەک کە دروستت کردووە کۆتایی هات
ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات
دەنگ
- داخراوە
+ کۆتایی هاتووە
کۆتایی دێت لە %s
- %s کەس
@@ -336,10 +336,10 @@
%1$s و %2$s
%1$s
پەسەندکراوە لەلایەن
- بەرزکراوە لەلایەن
+ پۆست کراوەتەوە لەلایەن
- - %s بەهێزکردن
- - %s بەهێزکردن
+ - %s پۆستکردنەوە
+ - %s پۆستکردنەوە
- %1$s دڵخواز
@@ -375,7 +375,6 @@
تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت
سیستەمی بنەڕەت
شێوازی ئیمۆجی
- ڕوونووسکراوە بۆ کلیپ بۆرد
نموونەکەت %s هیچ ئیمۆجییەکی ئاسایی نییە
دروستکردن
کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 7840aa9c..badd5f21 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -302,7 +302,6 @@
Kopie vašeho tootu byla uložena do vašich konceptů
Napsat
Vaše instance %s nemá žádná vlastní emoji
- Zkopírováno do schránky
Styl emoji
Výchozí nastavení systému
Musíte si nejprve stáhnout tyto sady emoji
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index ca22d127..4b9d9a62 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -251,7 +251,6 @@
Cadwyd copi o\'r tŵt i\'ch drafftiau
Creu
Nid oes gan eich achos %s emoji bersonol
- Copïwyd i\'r clipfwrdd
Arddull emoji
Rhagosodiad system
Bydd angen i chi lawrlwytho\'r setiau emoji hyn yn gyntaf
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index fef59401..fe8f871a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -25,7 +25,7 @@
Föderiert
Direktnachrichten
Tabs
- Beitrag
+ Konversation
Beiträge
mit Antworten
Angeheftet
@@ -208,7 +208,7 @@
Neue Erwähnungen
Benachrichtigungen über neue Erwähnungen
Neue Folgende
- Benachrichtigunen über neue Folgende
+ Benachrichtigungen über neue Folgende
Geteilte Beiträge
Benachrichtigungen, wenn deine Beiträge geteilt werden
Favorisierte Beiträge
@@ -279,7 +279,6 @@
Eine Kopie des Beitrags wurde in deine Entwürfe gespeichert
Beitrag erstellen
Deine Instanz %s hat keine Emojis definiert
- In die Zwischenablage kopiert
Emoji-Stil
System-Standard
Du musst diese Emoji-Sets zunächst herunterladen
@@ -491,7 +490,7 @@
Für immer
Anhänge
Audio
- Benachrichtigungen, wenn jemand, den ich abonniert habe, etwas Neues veröffentlicht
+ Benachrichtigungen, wenn jemand, den ich abonniert habe, eine neue Nachricht veröffentlicht
Neue Beiträge
GIF-Emojis animieren
Jemand, den ich abonniert habe, hat etwas Neues veröffentlicht
@@ -528,4 +527,14 @@
14 Tage
180 Tage
Beitrag erstellen
+ %s hat den Beitrag bearbeitet
+ Ein Beitrag, mit dem ich interagiert habe, wurde bearbeitet
+ Registrierungen
+ Benachrichtigungen über neue Profile
+ %s hat sich registriert
+ Jemand hat sich registriert
+ Benachrichtigungen, wenn Beiträge bearbeitet werden, mit denen du interagiert hast
+ Anmelden
+ Die Anmeldeseite konnte nicht geladen werden.
+ Beitragsbearbeitungen
\ No newline at end of file
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 4734fea9..85583a2b 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -3,4 +3,106 @@
Αυτό δεν μπορεί να είναι κενό.
Προέκυψε σφάλμα δικτύου! Παρακαλώ ελέγξτε τη σύνδεσή σας και προσπαθήστε ξανά!
Προέκυψε ένα σφάλμα.
+ Αποκλεισμένοι χρήστες
+ Ακύρωση αιτήματος ακολούθησης;
+ Διαγραφή αυτής της συζήτησης;
+ Δεν υπάρχουν αποτελέσματα
+ Επεργασία προφίλ
+ Ακολουθεί
+ Επαναφορά
+ ο/η %s σας ακολούθησε
+ Χρήστες σε σίγαση
+ Αποσύνδεση
+ Μην ακολουθείτε
+ Άρση σίγασης του %s
+ Διαγραφή και αναδιατύπωση
+ Επεξεργασία προφίλ
+ Κοινοποίηση
+ Άδειες
+ Ανοίξτε σε browser
+ Αιτήματα ακολούθησης
+ Προσθήκη σελιδοδείκτη
+ Περισσότερα
+ Σελιδοδείκτες
+ Σελιδοδείκτες
+ Ακόλουθοι
+ Άρση αποκλεισμού
+ Αγαπημένα
+ Η δημοσίευση είναι πολύ μεγάλη!
+ Πληκτρολόγιο emoji
+ Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε από τον λογαριασμό %1$s;
+ Προσχέδια
+ Αγαπημένα
+ Απάντηση…
+ Απόρριψη
+ Αποκλεισμένοι χρήστες
+ Αφαίρεση προώθησης
+ Επεξεργασία
+ Σίγαση του %s
+ Αποκλεισμός
+ Αναίρεση
+ ο/η %s ζήτησε να σας ακολουθήσει
+ Απάντηση
+ Καρτέλες
+ ο/η %s προώθησε τη δημοσίευσή σας
+ στον/στην %s άρεσε η δημοσίευσή σας
+ Ακολουθήστε
+ Αναφορά
+ Σίγαση
+ Τα μουσικά αρχεία πρέπει να είναι μικρότερα από 40MB.
+ Αφαίρεση αγαπημένου
+ Αναφορά του/της %s
+ Προτιμήσεις Λογαριασμού
+ Προσθήκη καρτέλας
+ Αντιγραφή συνδέσμου
+ Αναζήτηση…
+ Αποδοχή
+ Εμφάνιση προωθήσεων
+ Προφίλ
+ Αιτήματα ακολούθησης
+ Αναζήτηση
+ Διαγραφή συζήτησης
+ Διαγραφή
+ ο/η %s μόλις δημοσίευσε
+ Αποθήκευση
+ Γρήγορη Απάντηση
+ Χρήστες σε σίγαση
+ Το αρχείο πρέπει να είναι μικρότερο από 8MB.
+ Απόκρυψη προωθήσεων
+ Προτιμήσεις
+ Σύνδεση
+ Ανακοινώσεις
+ Προσχέδια
+ ο/η %s έκανε εγγραφή
+ Προσπαθήστε ξανά
+ Διαγραφή αυτής της δημοσίευσης;
+ Άρση σίγασης
+ Αγαπημένο
+ Σύνδεσμοι
+ Κλείσιμο
+ Ειδοποιήσεις
+ Γράψτε
+ Σύνδεση με Mastodon
+ Επεξεργασία
+ Προώθηση
+ Άρση ακολούθησης αυτού του λογαριασμού;
+ Απόκρυψη ειδοποιήσεων
+ Αφαίρεση σελιδοδείκτη
+ Προειδοποίηση περιεχομένου
+ Σύνδεσμοι
+ Σύνδεση…
+ Προγραμματισμένες δημοσιεύσεις
+ Προγραμματισμός δημοσίευσης
+ Προγραμματισμένες δημοσιεύσεις
+ Δημοσιεύσεις
+ Καρφιτσωμένο
+ Ευαίσθητο περιεχόμενο
+ Κρυμμένα μέσα
+ ο/η %s το προώθησε
+ Με απαντήσεις
+ Δείτε περισσότερα
+ Δείτε λιγότερα
+ Κλικ για να δείτε
+ ο/η %s επεξεργάστηκε τη δημοσίευσή του/της
+ Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης;
\ No newline at end of file
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index 5b47ccd0..891c96b8 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -299,7 +299,6 @@
Kopio de la mesaĝo estis konservita en viaj malnetoj
Verki
Via nodo %s ne havas proprajn emoĝiojn
- Kopiita en tondujo
Stilo de emoĝioj
Sistema valoro
Vi unue devos elŝuti ĉi tiujn emoĝiarojn
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 43b072a5..788d643f 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -269,7 +269,6 @@
Una copia del estado se ha guardado en borradores
Redactar
Su instancia %s no ofrece emojis personalizados
- Copiado al portapapeles
Estilo de los emojis
Sistema
Tendrás que descargarlos primero
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 791575b7..fd0c057a 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -253,7 +253,6 @@
Tutaren kopia zirriborroetan sartu da
Idatzi
%s instantziak ez ditu emoji pertsonalizatuak eskaintzen
- Arbelean kopiatua
Emojien estiloa
Sistema
Lehenago jaitsi beharko dituzu
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 880e0e4a..d0b2556d 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -248,7 +248,6 @@
رونوشتی از بوق در پیشنویسهایتان ذخیره شد
ایجاد
نمونهتان %s هیچ اموجی سفارشیای ندارد
- در تختهگیره رونوشت شد
سبک اموجی
پیشگزیدهٔ سامانه
نخست باید این مجموعههای اموجی را بارگیری کنید
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index a7f320a4..987e777b 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -295,7 +295,7 @@
Mettre une légende
Supprimer le média
Verrouiller le compte
- Vous devez approuvez manuellement les abonnements
+ Vous devez approuver manuellement les abonnements
Enregistrer comme brouillon ?
Envoi du pouet…
Erreur lors de l’envoi du pouet
@@ -304,7 +304,6 @@
Une copie du pouet a été sauvegardée dans vos brouillons
Écrire
Votre instance %s n’a pas d’émojis personnalisés
- Copié dans le presse-papier
Style d’émojis
Par défaut du système
Vous devez commencer par télécharger ces jeux d’émojis
@@ -540,4 +539,13 @@
14 jours
180 jours
Rédiger un message
+ %s a créé un compte
+ Nouveaux comptes
+ Notifications quand quelqu\'un crée un nouveau compte
+ un nouveau compte a été créé
+ %s a modifié son message
+ un message avec lequel j\'ai interagi est modifié
+ Messages modifiés
+ Notifications quand un post avec lequel vous avez interagi est modifié
+ Se connecter
\ No newline at end of file
diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml
index 97ad4052..f902d742 100644
--- a/app/src/main/res/values-fy/strings.xml
+++ b/app/src/main/res/values-fy/strings.xml
@@ -4,7 +4,6 @@
Dit mei net leech wêze.
Systeem standert
Emoji styl
- Nei it klemboerd kopiearre
Gearstelle
Ferstjoeren ôfbrutsen
Toots oan it ferstjoeren
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index 9342edf9..121ed937 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -339,7 +339,6 @@
Sábháladh cóip den tút ar do dhréachtaí
Cum
Níl aon emojis saincheaptha ag do shampla %s
- Cóipeáladh chuig an gearrthaisce
Stíl Emoji
Réamhshocrú an chórais
Beidh ort na tacair emoji seo a íoslódáil ar dtús
diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml
index bf2a2d52..317331dc 100644
--- a/app/src/main/res/values-gd/strings.xml
+++ b/app/src/main/res/values-gd/strings.xml
@@ -92,7 +92,7 @@
Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr
Postaichean ùra
dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr
- Tha %s air rud a phostadh
+ Phostaich %s rud
Chan eil brath-fios ann.
Brathan-fios
Chaidh a shàbhaladh!
@@ -239,7 +239,6 @@
Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach
Bun-roghainn an t-siostaim
Stoidhle nan Emojis
- Chaidh lethbhreac dheth a chur air an stòr-bhòrd
Chan eil Emojis gnàthaichte aig an ionstans %s agad
Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd
Chaidh sgur dhen chur
@@ -249,10 +248,16 @@
A bheil thu airson a shàbhaladh ’na dhreachd\?
Feumaidh tu gabhail ri luchd-leantainn ùr a làimh
Glais an cunntas
- Suidhidh am fo-thiotal
+ Suidhich am fo-thiotal
- Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
-\n(%d caractar(an) air a char as fhaide)
+\n(%d charactar air a char as fhaide)
+ - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
+\n(%d charactar air a char as fhaide)
+ - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
+\n(%d caractaran air a char as fhaide)
+ - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
+\n(%d caractar air a char as fhaide)
Cha deach leinn am fo-thiotal a shuidheachadh
A’ postadh leis a’ chunntas %1$s
@@ -290,7 +295,7 @@
Cuir post air an sgeideal
Faicsinneachd a’ phuist
Postaichean air an sgeideal
- Chuir %s am post agad ris na h-annsachdan
+ Is annsa le %s am post agad
Bhrosnaich %s am post agad
Postaichean air an sgeideal
Snàithlean
@@ -323,7 +328,7 @@
an ceann %du
an ceann %dl
an ceann %db
- Iarrar leantainn orm
+ Iarrtas leantainn air
Videothan
Dealbhan
Pròifil Tusky
@@ -541,4 +546,15 @@
14 làithean
60 latha
Sgrìobh post
+ Chlàraich %s
+ Clàraidhean
+ Brathan mu cleachdaichean ùra
+ chlàraich cuideigin
+ Dheasaich %s am post aca
+ Deasachadh puist
+ Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh
+ chaidh post a rinn mi conaltradh leis a deasachadh
+ Clàraich a-steach
+ Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh.
+ A’ sàbhaladh na dreuchd…
\ No newline at end of file
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index bfabe4c8..5b488df3 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -247,7 +247,6 @@
Deberás descargar primeiro estos conxuntos de emojis
Por defecto no sistema
Estilo dos emoji
- Copiado ao portapapeis
A túa instancia %s non ten emojis personalizados
Redactar
Gardouse unha copia do toot nos borradores
@@ -519,4 +518,8 @@
180 días
365 días
Redactar publicación
+ %s rexistrouse
+ hai unha nova usuaria
+ Rexistros
+ Notificacións sobre novas usuarias
\ No newline at end of file
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 16882504..8f537f12 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -306,7 +306,6 @@
बाद में
एप्लिकेशन को पुनः आरंभ की आवश्यकता है
आपको पहले इस इमोजी सेट को डाउनलोड करना होगा
- क्लिपबोर्ड पर कॉपी किया गया
लिखें
टूट की एक प्रति आपके ड्राफ्ट में सहेज ली गई है
टूट भेजने में त्रुटि
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 7371cbe7..728f9ae9 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -9,26 +9,26 @@
Azonosítatlan engedélyezési hiba történt.
Engedély megtagadva.
Bejelentkezési token megszerzése sikertelen.
- Túl hosszú a tülkölés!
+ Túl hosszú a bejegyzés!
A fájlnak kisebbnek kell lennie, mint 8 MB.
A videofájloknak kisebbnek kell lenniük, mint 40 MB.
Ilyen típusú fájlt nem lehet feltölteni.
Fájl megnyitása sikertelen.
Média olvasási engedély szükséges.
Média tárolási engedély szükséges.
- Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez.
+ Képek és videók egyszerre nem csatolhatóak ugyanazon bejegyzéshez.
Feltöltés sikertelen.
- Nem sikerült elküldeni a tülköt.
+ Nem sikerült elküldeni a bejegyzést.
Kezdőlap
Értesítések
Helyi
Föderációs
Közvetlen üzenetek
Fülek
- Tülk
- Tülkök
+ Szál
+ Bejegyzések
Válaszokkal
- Rögzített
+ Kitűzött
Követett
Követő
Kedvencek
@@ -40,17 +40,17 @@
Licenszek
\@%s
%s megtolta
- Kényes tartalom
+ Érzékeny tartalom
Rejtett média
- Kattints a megnézéshez
+ Kattints a megtekintéshez
Mutass többet
Mutass kevesebbet
Kibontás
Összecsukás
Nincs itt semmi.
Üres tartalom. Húzd le a frissítéshez!
- %s megtolta a tülködet
- %s kedvencnek jelölte tülködet
+ %s megtolta a bejegyzésedet
+ %s kedvencnek jelölte a bejegyzésedet
%s bekövetett
\@%s jelentése
Egyéb megjegyzés?
@@ -100,7 +100,7 @@
Elutasítás
Keresés
Piszkozatok
- Tülkök láthatósága
+ Bejegyzés láthatósága
Tartalom figyelmeztetés
Emoji billentyűzet
Fül hozzáadása
@@ -115,8 +115,8 @@
Link másolása
Megnyitás mint %s
Megosztás mint …
- Tülk URL megosztása…
- Tülk megosztása…
+ Bejegyzés URL megosztása…
+ Bejegyzés megosztása…
Elküldve!
Felhasználó letiltása feloldva
Felhasználó némítása feloldva
@@ -132,7 +132,7 @@
Válasz…
Profilkép
Fejléc
- Mi az a szerver\?
+ Mi az a példány\?
Csatlakozás…
Bármely példány címét vagy domain nevét beírhatod ide, mint a mastodon.social, az icosahedron.website, a social.tchncs.de és mások!
\n
@@ -146,11 +146,11 @@
Letöltés
Visszavonod a követési kérelmet?
Követés megszüntetése?
- Törlöd ezt a tülköt?
- Nyilvános: Tülkölés nyilvános idővonalra
+ Törlöd ezt a bejegyzést\?
+ Nyilvános: Bejegyzés nyilvános idővonalra
Listázatlan: Nem jelenik meg a nyilvános idővonalon
- Csak követőknek: Tülkölés csak követőknek
- Közvetlen: Tülkölés csak a megemlített felhasználóknak
+ Csak követőknek: Bejegyzés csak követőknek
+ Közvetlen: Bejegyzés csak a megemlített felhasználóknak
Értesítések
Értesítések
Figyelmeztetések
@@ -160,8 +160,8 @@
Értesítsen, ha
megemlítettek
bekövettek
- tülkömet megtolták
- tülkömet kedvenccé tették
+ bejegyzésemet megtolták
+ bejegyzésemet kedvencnek jelölték
Megjelenés
Idővonalak
Sötét
@@ -181,13 +181,13 @@
HTTP proxy engedélyezése
HTTP proxy szerver
HTTP Proxy port
- Tülkök alapértelmezett láthatósága
+ Bejegyzések alapértelmezett láthatósága
Minden média kényesnek jelölése
A beállítások szinkronizálása nem sikerült
Nyilvános
Listázatlan
Csak követőknek
- Tülkölés szöveg mérete
+ Bejegyzés szövegének mérete
Legkisebb
Kicsi
Közepes
@@ -198,9 +198,9 @@
Új követők
Értesítések új követőkről
Megtolások
- Értesítések tülkjeid megtolása esetén
+ Értesítések bejegyzéseid megtolása esetén
Kedvencek
- Értesítések mikor tülkjeidet kedvencnek jelölik
+ Értesítések amikor a bejegyzéseidet kedvencnek jelölik
%s megemlített téged
%1$s, %2$s, %3$s és még %4$d
%1$s, %2$s meg %3$s
@@ -224,10 +224,10 @@
Hibajelentés & új funkciók igénylése:
\n https://git.chinwag.org/chinwag/chinwag-android/issues
Tusky profilja
- Tülk tartalmának megosztása
- Tülk linkjének megosztása
+ Bejegyzés tartalmának megosztása
+ Bejegyzés hivatkozásának megosztása
Képek
- Videók
+ Videó
Követés kérelmezve
Követ téged
@@ -241,19 +241,18 @@
Törlés
Fiók zárolása
Elmented a piszkozatot\?
- Tülk elküldése…
- A tülk elküldése nem sikerült
- Tülkök elküldése
+ Bejegyzés küldése…
+ A bejegyzés elküldése sikertelen
+ Bejegyzések elküldése
Küldés megszakítva
- A tülk másolatát elmentettük a piszkozataid közé
+ A bejegyzés másolatát elmentettük a piszkozataid közé
Szerkesztés
- A %s szervernek nincsenek egyedi emoji-jai
- Vágólapra másolva
+ A %s példánynak nincsenek egyedi emoji-jai
Emoji stílus
Rendszer alapértelmezés
Először le kell töltened ezeket az emoji készleteket
Keresés…
- Tülk megnyitása
+ Bejegyzés megnyitása
Az app újraindítása szükséges
A beállítások érvényesítéséhez újra kell indítani a Tuskyt
Később
@@ -280,8 +279,7 @@
- elérted a fülek maximális számát (%1$d)
- elérted a fülek maximális számát (%1$d)
- Nincs leírás
-
+ Nincs leírás
Nyilvános
Követők
Kedvenc eltávolítása
@@ -290,7 +288,7 @@
Média letöltése
Média letöltése
Média megosztása következővel…
- Törlöd és újraírod ezt a tülköt\?
+ Törlöd és újraírod ezt a bejegyzést\?
befejeződött egy szavazás
Szűrők
Rendszer téma használata
@@ -342,7 +340,7 @@
Általad követettek keresése
Fiók hozzáadása a listához
Fiók eltávolítása a listából
- Tülkölés %1$s fiókkal
+ Bejegyzés %1$s fiókkal
Cím beállítása nem sikerült
- Leírás látássérülteknek
@@ -350,7 +348,7 @@
Cím beállítása
Minden követődet külön engedélyezned kell
- Minden tülk kibontása/összecsukása
+ Összes bejegyzés kibontása/összecsukása
A Google jelenlegi emodzsi készlete
Megtolás az eredeti közönségnek
Megtolás visszavonása
@@ -368,7 +366,7 @@
%1$s
%1$s, %2$s és még %3$d
Média: %s
- Tartalom figyelmeztetés: %s
+ Tartalomfigyelmeztetés: %s
Megtolt
Kedvelt
Listázatlan
@@ -379,7 +377,7 @@
Törlés
Szűrés
Alkalmaz
- Tülk szerkesztése
+ Bejegyzés létrehozása
Szerkesztés
Biztos, hogy minden értesítésedet véglegesen törlöd\?
Műveletek a(z) %s képpel
@@ -416,11 +414,11 @@
Egyéb megjegyzések
Továbbítás neki %s
Nem sikerült a bejelentés
- Nem sikerült a tülkök letöltése
+ Sikertelen a bejegyzések letöltése
A bejelentést a szervered moderátorának küldjük el. Alább megadhatsz egy magyarázatot arra, hogy miért jelented be ezt a fiókot:
A fiók egy másik szerverről származik. Küldjünk oda is egy anonimizált másolatot a bejelentésről\?
Értesítések szűrőjének mutatása
- Tartalom-figyelmeztetéssel ellátott tülkök kifejtése mindig
+ Tartalomfigyelmeztetéssel ellátott bejegyzések kinyitása mindig
Fiókok
Sikertelen keresés
Szavazás hozzáadása
@@ -436,12 +434,12 @@
Több lehetőség
Válasz %d
Szerkesztés
- Időzített tülkök
+ Időzített bejegyzések
Szerkesztés
- Időzített tülkök
- Tülk Időzítése
+ Időzített bejegyzések
+ Bejegyzés Időzítése
Visszaállítás
- Nem találjuk ezt a tülköt %s
+ Nem találjuk ezt a bejegyzést %s
Könyvjelzők
Könyvjelzőzés
Könyvjelzők
@@ -451,7 +449,7 @@
Lista
A hangfájloknak kisebbnek kell lenniük, mint 40 MB.
Nincs egy piszkozatod sem.
- Nincs egy ütemezett tülköd sem.
+ Nincs egy ütemezett bejegyzésed sem.
A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc.
Követési kérelmek
Jóváhagyó ablak mutatása megtolás előtt
@@ -484,34 +482,34 @@
Saját, mások számára nem látható megjegyzés erről a fiókról
Nincsenek közlemények.
Közlemények
- A tülköt, melyre válaszul piszkozatot készítettél törölték
+ A bejegyzést, melyre válaszul piszkozatot készítettél törölték
Piszkozat törölve
Nem sikerült a Válasz információit betölteni
- Ez a tülk nem küldődött el!
+ Ezt a bejegyzést nem tudtuk elküldeni!
Tényleg le akarod törölni a %s listát\?
- Nem tölthetsz fel %1$d médiacsatolmányból többet.
- Nem tölthetsz fel %1$d médiacsatolmányból többet.
Profilok mérőszámainak elrejtése
- Tülkök mérőszámainak elrejtése
+ Bejegyzések mérőszámainak elrejtése
Idővonali értesítések korlátozása
Értesítések Áttekintése
- Pár információ, ami befolyásolhatja a mentális egészségedet rejtve marad. Ilyenek pl.:
+ Pár információ, ami befolyásolhatja a mentális jóllétedet rejtve marad. Ilyenek pl.:
\n
\n - Kedvenc/Megtolás/Bekövetés értesítései
-\n - Kedvenc/Megtolás számlálók a tülkökön
-\n - Követő/Tülk statisztikák a profilokon
+\n - Kedvenc/Megtolás számlálók a bejegyzéseken
+\n - Követő/Bejegyzés statisztikák a profilokon
\n
\nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat.
Végtelen
Időtartam
Csatolmányok
Audio
- Értesítések általam követett személy új tülkjeiről
- Új tülkök
- valaki, akit követek újat tülkölt
- %s épp tülkölt
+ Értesítések általam követett személy új bejegyzéseiről
+ Új bejegyzések
+ valaki, akit követek új bejegyzést tett közzé
+ %s épp bejegyzést írt
Jóllét
Egyedi emojik animálása
Leiratkozás
@@ -521,4 +519,19 @@
Beszélgetés törlése
Könyvjelző törlése
Jóváhagyás mutatása kedvencnek jelölés előtt
+ %s szerkesztette a bejegyzését
+ szerkesztették a bejegyzést, mellyel dolgod volt
+ %s regisztrált
+ valaki regisztrált
+ Regisztrációk
+ Értesítések új felhasználókról
+ 14 nap
+ 30 nap
+ 60 nap
+ 90 nap
+ 180 nap
+ 365 nap
+ Bejegyzések szerkesztése
+ Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt
+ Bejegyzés Létrehozása
\ No newline at end of file
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index 2018dc85..f6241b86 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -317,7 +317,6 @@
Afrit af tístinu þínu hefur verið vistað drögunum þínum
Semja skilaboð
Tilvikið þitt %s er ekki með nein sérsniðin tjáningartákn
- Afritað á klippispjald
Stíll tjáningartákna
Sjálfgefið í kerfinu
Þú þarft fyrst að ná í þessi táknmyndasett
@@ -519,4 +518,12 @@
365 dagar
14 dagar
Semja færslu
+ %s skráði sig
+ einhver skráði sig
+ %s breytti færslunni sinni
+ færsla sem ég hef átt við er breytt
+ Nýskráningar
+ Tilkynningar um nýja notendur
+ Breytingar á færslum
+ Tilkynningar þegar færslum sem þú hefur átt við er breytt
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index af39264f..fcd93b10 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -2,33 +2,33 @@
Si è verificato un errore.
Si è verificato un errore di rete! Per favore controlla la tua connessione e riprova!
- Questo non può esser vuoto.
- Inserito un dominio non valido
- Autenticazione fallita con quell\'istanza.
- Non riesco a trovare un browser web da usare.
+ Questo non può essere vuoto.
+ Inserito dominio non valido
+ Autenticazione con quell\'istanza fallita.
+ Nessun browser web utilizzabile trovato.
Si è verificato un errore di autenticazione non identificato.
- L\'autorizzazione è stata negata.
- Errore nell\'acquisizione del token di accesso.
- Lo stato è troppo lungo!
- La dimensione dei file immagine deve essere inferiore a 8 MB.
- La dimensione dei file video deve essere inferiore a 40 MB.
- Questo tipo di file non può essere caricato.
- Questo file non può essere aperto.
- Il permesso di lettura della scheda sd è richiesto.
- È richiesta l\'autorizzazione di archiviazione.
- Immagini e video non possono essere allegati allo stesso stato.
- Il caricamento non è riuscito.
- Errore nell\'invio del toot.
+ Autorizzazione negata.
+ Acquisizione token di accesso fallita.
+ Il post è troppo lungo!
+ Il file deve essere più piccolo di 8 MB.
+ I video devono essere più piccoli di 40 MB.
+ Quel tipo di file non può essere caricato.
+ Non è stato possibile aprire quel file.
+ È richiesto il permesso di leggere file.
+ È richiesto il permesso di salvare file.
+ Non è possibile allegare allo stesso post immagini e video.
+ Il caricamento è fallito.
+ Errore nell\'invio del post.
Home
Notifiche
Locale
Federata
- Messaggi Diretti
+ Messaggi diretti
Schede
- Toot
+ Conversazione
Post
Con risposte
- Fissati in alto
+ Fissati
Seguiti
Seguono
Preferiti
@@ -43,15 +43,15 @@
Contenuto sensibile
Media nascosto
Clicca per visualizzare
- Mostra di Più
- Mostra Meno
+ Mostra di più
+ Mostra di meno
Espandi
Riduci
- Qui non c\'è niente.
- Qui non c\'è niente. Trascina verso il basso per aggiornare!
- %s ha boostato il tuo toot
- %s ha messo il tuo toot nei preferiti
- %s ti segue
+ Qui non c\'è nulla.
+ Qui non c\'è nulla. Trascina verso il basso per aggiornare!
+ %s ha boostato il tuo post
+ %s ha messo il tuo post nei preferiti
+ %s ti ha seguito
Segnala @%s
Commenti aggiuntivi?
Risposta veloce
@@ -79,7 +79,7 @@
Chiudi
Profilo
Preferenze
- Preferenze Account
+ Preferenze account
Preferiti
Utenti silenziati
Utenti bloccati
@@ -102,14 +102,14 @@
Rifiuta
Cerca
Bozze
- Visibilità dei toot
- Avviso per il contenuto
+ Visibilità dei post
+ Avviso di contenuto sensibile
Tastiera emoji
- Aggiungi Scheda
+ Aggiungi scheda
Collegamenti
Menzioni
Hashtag
- Apri autore del boost
+ Vai all\'autore del boost
Mostra boost
Mostra preferiti
Hashtag
@@ -117,11 +117,11 @@
Collegamenti
Apri media #%d
Scaricando %1$s
- Copia il link
+ Copia link
Apri come %s
Condividi come …
- Condividi URL del toot su…
- Condividi toot su…
+ Condividi URL del post su…
+ Condividi post su…
Condividi media su…
Inviato!
Utente sbloccato
@@ -132,7 +132,7 @@
Cosa succede?
Avviso di contenuto sensibile
Mostra nome
- Bio
+ Biografia
Cerca…
Nessun risultato
Rispondi…
@@ -152,22 +152,22 @@
Scarica
Revocare la richiesta di seguire?
Smettere di seguire questo account?
- Eliminare questo toot?
+ Eliminare questo post\?
Pubblico: visibile sulla timeline pubblica
- Non Elencato: non visibile sulla timeline pubblica e locale
- Solo Follower: visibile solo dai tuoi follower
+ Non in elenco: non visibile sulla timeline pubblica e locale
+ Solo follower: visibile solo dai tuoi follower
Diretto: visibile solo agli utenti menzionati
- Modifica Notifiche
+ Notifiche
Notifiche
Allarmi
Notifica con suoneria
Notifica con vibrazione
Notifica con luce
Notificami quando
- sono stato menzionato
- sono stato seguito
- i miei post sono boostati
- i miei post sono messi nei preferiti
+ vengo menzionato
+ vengo seguito
+ i miei post vengono boostati
+ i miei post vengono messi nei preferiti
Aspetto
Tema dell\'app
Timeline
@@ -176,10 +176,10 @@
Chiaro
Nero
Automatico al tramonto
- Usa Tema di Sistema
+ Usa tema di sistema
Browser
- Usa Tab Personalizzate di Chrome
- Nascondi il pulsante componi mentre scorri
+ Usa Custom Tabs di Chrome
+ Nascondi il pulsante Componi mentre scorri
Lingua
Filtraggio della timeline
Schede
@@ -193,25 +193,25 @@
Porta proxy HTTP
Privacy di default dei post
Segna sempre media come contenuto sensibile
- Pubblicando (sincronizzato con il server)
+ Pubblicazione (sincronizzato con il server)
Sincronizzazione delle impostazioni fallita
Pubblico
- Non elencato
- Solo per chi ti segue
- Dimensione del testo degli stati
+ Non in elenco
+ Solo follower
+ Dimensione del testo dei post
Piccolissimo
Piccolo
Normale
Grande
Grandissimo
- Nuove Menzioni
- Notifiche quando qualcuno ti menziona
- Nuove persone che ti seguono
- Notifiche su nuove persone che ti seguono
+ Nuove menzioni
+ Notifiche di quando vieni menzionato da qualcuno
+ Nuovi follower
+ Notifiche su nuovi follower
Boost
- Notifiche quando i tuoi toot vengono boostati
+ Notifiche sui tuoi post che vengono boostati
Preferiti
- Notifiche quando i tuoi toot vengono segnati come preferiti
+ Notifiche sui tuoi post che vengono segnati come preferiti
%s ti ha menzionato
%1$s, %2$s, %3$s e %4$d altri
%1$s, %2$s e %3$s
@@ -234,14 +234,14 @@
-->
Sito web del progetto:\n
https://chinwag.org
- Segnala problemi & richiedi funzionalità:\n
- https://git.chinwag.org/chinwag/chinwag-android/issues
+ Segnala problemi e richiedi funzionalità:
+\n https://git.chinwag.org/chinwag/chinwag-android/issues
Profilo di Tusky
- Condividi contenuto del toot
- Condividi link al toot
+ Condividi contenuto del post
+ Condividi link al post
Immagini
Video
- In attesa di approvazione
+ Richiesta inviata
in %d a
in %dg
@@ -250,14 +250,14 @@
in %ds
%da
%dg
- %d o
- %d min
- %d s
- Seguono te
- Mostra sempre tutto il contenuto sensibile
+ %do
+ %dmin
+ %ds
+ Ti segue
+ Mostra sempre tutti i contenuti sensibili
Media
Rispondendo a @%s
- carica di più
+ carica altri
Timeline pubbliche
Conversazioni
Aggiungi filtro
@@ -265,7 +265,7 @@
Rimuovi
Aggiorna
Frase da filtrare
- Aggiungi Account
+ Aggiungi account
Aggiungi un nuovo Account Mastodon
Liste
Liste
@@ -288,30 +288,29 @@
Inserisci descrizione
Rimuovi
Blocca account
- Richiede la tua approvazione manuale di chi ti segue
+ Richiedi una tua approvazione manuale per seguirti
Salvare bozza?
- Inviando il Toot…
+ Inviando il post…
Errore durante l\'invio
- Invio Toot
+ Invio post
Invio annullato
- Una copia del toot è stata salvata nelle tue bozze
+ Una copia del post è stata salvata nelle tue bozze
Componi
La tua istanza %s non ha nessuna emoji personalizzata
- Copiato negli appunti
- Stile di emoji
- Predefiniti del sistema
+ Stile delle emoji
+ Predefinite del sistema
Dovrai prima scaricare questo pacchetto di emoji
- Eseguendo una ricerca…
- Espandi/Riduci tutti gli stati
- Apri toot
+ Ricerca in corso…
+ Espandi/riduci tutti i post
+ Apri post
Riavvio dell\'app richiesto
Devi riavviare Tusky per applicare queste modifiche
Più tardi
Riavvia
Le emoji predefinite del tuo dispositivo
- Le emoji Blob conosciute da Android 4.4-7.1
+ Le emoji Blob di Android 4.4-7.1
Le emoji standard di Mastodon
- Scaricamento fallito
+ Download fallito
Bot
%1$s si è spostato su:
Boost con la visibilità del post di origine
@@ -324,63 +323,60 @@
aggiungi dati
Etichetta
Contenuto
- Usa tempo assoluto
+ Usa ora assoluta
Il profilo dell\'utente mostrato qui sotto potrebbe essere incompleto. Premi per aprire il profilo completo nel browser.
- Non fissare
+ Smetti di fissare
Fissa
- - %1$s Mi piace
- - %1$s Mi piace
+ - %1$s Preferito
+ - %1$s Preferiti
- <b>%s</b> Boost
- <b>%s</b> Boost
Boostato da
- Preferito da
+ Aggiunto ai preferiti da
%1$s
%1$s e %2$s
%1$s, %2$s ed altri %3$d
- - limite massimo di %1$d tab raggiunto
- - limite massimo di %1$d tab raggiunto
+ - limite massimo di %1$d scheda raggiunto
+ - limite massimo di %1$d schede raggiunto
- Media: %s
-
+ Media: %s
Contenuto sensibile: %s
Nessuna descrizione
Ribloggato
- Apprezzato
-
+ Messo nei preferiti
Pubblico
- Non elencato
-
- Seguaci
+ Non in elenco
+ Solo follower
Diretti
Nome della lista
Scarica media
Scaricando media
- Componi Toot
+ Componi post
Hashtag senza #
Componi
- Pulisci
+ Svuota
Filtra
Applica
- Mostra indicatore per bot
+ Mostra indicatore bot
Sei sicuro di voler permanentemente eliminare tutte le tue notifiche\?
Cancella e riscrivi
- Cancellare e riscrivere questo toot\?
+ Cancellare e riscrivere questo post\?
- %s voto
- %s voti
- termina alle %s
- terminato
+ si conclude alle %s
+ concluso
Vota
Domini nascosti
Domini nascosti
@@ -388,37 +384,37 @@
%s mostrati
Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi.
Nascondi l\'intero dominio
- Le votazioni sono finite
- Mostra le animazioni delle GIF negli avatar
+ dei sondaggi si sono conclusi
+ Riproduci animazioni avatar
Votazioni
- Notifiche sulle votazioni che sono concluse
+ Notifiche sulle votazioni che si sono concluse
Parola intera
Quando la parola chiave o la frase sono composte da soli caratteri alfanumerici, sarà applicata solo se corrisponde alla parola completa
- Insieme di emoji di Google
+ Set di emoji di Google
Segnalibri
Segnalibro
Modifica
Segnalibri
Aggiungi sondaggio
- Fatto con Tusky
- Espandi sempre i toot segnalati come contenuto sensibile
- Messo nei segalibri
+ Fatto usando Tusky
+ Espandi sempre i post segnalati come contenuto sensibile
+ Messo nei segnalibri
Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s
Scegli lista
Lista
Azioni per l\'immagine %s
- Un sondaggio che hai votato è terminato
- Un sondaggio che hai creato è terminato
+ Un sondaggio che hai votato si è concluso
+ Un sondaggio che hai creato si è concluso
- - %d giorno rimasti
+ - %d giorno rimasto
- %d giorni rimasti
- - %d ora rimasti
+ - %d ora rimasta
- %d ore rimasti
- - %d minuto rimasti
+ - %d minuto rimasto
- %d minuti rimasti
@@ -428,12 +424,12 @@
Continua
Indietro
Fatto
- Inviato con successo @%s
+ Segnalato @%s con successo
Altri commenti
Inoltra a %s
- Errore durante l\'invio
- Errore durante lo scaricamento degli aggiornamenti
- La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè vuoi segnalare questo utente qui sotto:
+ Segnalazione fallita
+ Scaricamento dei post fallito
+ La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè stai segnalando questo utente qui sotto:
L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\?
Utenti
Errore durante la ricerca
@@ -451,10 +447,10 @@
Scelta %d
Modifica
Errore nella ricerca del post %s
- Toot programmati
- Toot programmati
- Programma un toot
- RIpristina
+ Post programmati
+ Post programmati
+ Programma un post
+ Ripristina
%1$s • %2$s
Non hai bozze.
@@ -465,66 +461,66 @@
Aggiungi hashtag
Silenziare @%s\?
Bloccare @%s\?
- Non silenziare più %s
- Smetti di silenziare conversazione
+ Smetti di silenziare %s
+ Smetti di silenziare la conversazione
Silenzia conversazione
%s ha chiesto di seguirti
- La dimensione dei file audio deve essere inferiore a 40 MB.
+ I file audio devono essere più piccoli di 40 MB.
Smetti di silenziare %s
Richieste di seguirti
Salvato!
La tua nota privata su questo account
Nascondi il titolo della barra degli strumenti in alto
- Mostra la finestra di dialogo di conferma prima del boosting
+ Mostra la finestra di conferma prima di boostare
Mostra le anteprime dei collegamenti nelle timelines
- Mastodon ha un intervallo minimo di programmazione di 5 minuti.
+ Mastodon ha un intervallo di programmazione minimo di 5 minuti.
Non ci sono annunci.
- Non hai stati pianificati.
+ Non hai post pianificati.
Abilita il gesto di scorrimento per passare da una scheda all\'altra
Notifiche sulle richieste di essere seguiti
- Parte inferiore
+ In fondo
In cima
- Posizione di navigazione principale
- Mostra sfumature colorate per i media nascosti
+ Posizione barra di navigazione principale
+ Mostra gradienti colorati per i media nascosti
Nascondi notifiche
Disattiva le notifiche da %s
Riattiva le notifiche da %s
Annunci
- Richieste di seguirti
+ mi viene richiesto di seguirmi
Nascondi statistiche quantitative sui profili
Nascondi le statistiche quantitative sui post
- Limita le notifiche della timeline
- Revisiona le notifiche
+ Limita le notifiche dalla timeline
+ Rivedi le notifiche
Benessere
- Notifiche di quando qualcuno a cui sei iscritto ha pubblicato un nuovo toot
- Nuovi toots
- qualcuno a cui sono iscritto ha pubblicato un nuovo toot
- %s appena pubblicato
+ Notifiche di nuovi post di qualcuno a cui sei iscritto
+ Nuovi post
+ qualcuno che seguo ha pubblicato un nuovo post
+ %s ha appena pubblicato
- Non puoi caricare più di %1$d allegato multimediale.
- Non puoi caricare più di %1$d allegati multimediali.
- Il toot a cui hai scritto una risposta è stato rimosso
- Bozza cancellata
- L\'invio di questo toot è fallito!
+ Il post a cui hai scritto una risposta è stato rimosso
+ Bozza eliminata
+ L\'invio di questo post è fallito!
Sei sicuro di voler cancellare la lista %s\?
Indefinita
Durata
Allegati
Audio
- Mostra le animazioni delle emojis personalizzate
+ Riproduci emoji animate
Iscriviti
Rimuovere questa conversazione\?
- Errore nel recuperare le informazioni sulla risposta
+ Errore nel recupero delle informazioni sulla risposta
Disiscriviti
- Rimuovi conversazione
+ Elimina conversazione
Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include:
\n
\n - Notifiche riguardo a Preferiti/Boost/Following
-\n - Conteggio dei Preferiti/Boost nei toot
-\n - Statistiche riguardo a Preferiti e Post nei profili
+\n - Conteggio dei Preferiti/Boost nei post
+\n - Statistiche riguardo a Preferiti/Post nei profili
\n
-\n Le notifiche push non saranno influenzate, ma puoi rivedere le tue impostazioni delle notifiche manualmente.
+\n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente.
Rimuovi segnalibro
Chiedi conferma prima di boostare
14 giorni
@@ -533,5 +529,16 @@
90 giorni
180 giorni
365 giorni
- Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler controllare queste richieste di following da parte questi account manualmente.
+ Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler verificare le richieste di seguirti da parte questi account manualmente.
+ %s si è registrato
+ qualcuno si è registrato
+ Login
+ %s ha modificato il suo post
+ un post con cui ho interagito è stato modificato
+ Componi post
+ Registrazioni
+ Notifiche di quando qualcuno si è registrato
+ Modifiche ai post
+ Notifiche di quando i post con cui hai interagito vengono modificati
+ Non è stato possibile caricare la pagina di login.
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index bb8c892f..ff978bf3 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -275,7 +275,6 @@
トゥートのコピーが下書きに保存されました
新規投稿
インスタンス %s にはカスタム絵文字がありません
- クリップボードにコピーされました
絵文字スタイル
システムのデフォルト
最初にこれらの絵文字セットをダウンロードする必要があります
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index e413d1af..6921eb4b 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -311,7 +311,6 @@
복사본이 임시 저장에 저장되었습니다
글쓰기
이 인스턴스 %s 은(는) 커스텀 이모지가 없습니다.
- 클립보드에 복사되었습니다
이모지 스타일
시스템 기본
시스템 기본 외의 이모지를 설정하시려면 우선 다운로드해야 합니다
diff --git a/app/src/main/res/values-night/theme_colors.xml b/app/src/main/res/values-night/theme_colors.xml
index 1054b5ee..59f71288 100644
--- a/app/src/main/res/values-night/theme_colors.xml
+++ b/app/src/main/res/values-night/theme_colors.xml
@@ -24,4 +24,7 @@
false
+ @color/white
+ @color/tusky_grey_10
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 1acee464..0552be7f 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -277,7 +277,6 @@
Een kopie van de toot werd opgeslagen als concept
Toot schrijven
Jouw server %s heeft geen lokale emojis
- Naar het klembord gekopieerd
Emojistijl
Systeemstandaard
Je moet eerst deze emoji-sets downloaden
diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml
index b9203d7a..8890c9dd 100644
--- a/app/src/main/res/values-no-rNB/strings.xml
+++ b/app/src/main/res/values-no-rNB/strings.xml
@@ -278,7 +278,6 @@
En kopi av tootet er lagret i kladdene dine
Skriv
Instansen %s har ingen egendefinerte emojis
- Kopiert til utklippstavlen
Emoji-stil
Systemstandard
Du må laste ned emoji-samlingene før de kan brukes
@@ -519,4 +518,14 @@
365 dager
14 dager
Komponer toot
+ %s registrerte seg
+ noen registrerte seg
+ Registreringer
+ Varslinger om nye brukere
+ %s redigerte innlegget sitt
+ et innlegg jeg har hatt en interaksjon med er redigert
+ Redigerte innlegg
+ Varslinger når et innlegg du har hatt en interaksjon med er redigert
+ Innlogging
+ Klarte ikke å laste innloggingssiden.
\ No newline at end of file
diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml
index c41fe5a2..89cbd0dc 100644
--- a/app/src/main/res/values-oc/strings.xml
+++ b/app/src/main/res/values-oc/strings.xml
@@ -243,7 +243,6 @@
Una còpia del tut es estat salvat dins los borrolhons
Redactar
L’instància %s es pas compatibla amb los emoji personalizats
- Copiat al quichapapièr
Estil dels Emoji
Çò del sistèma
D’en primièr vos cal telecargar los emojis seguents
@@ -337,8 +336,8 @@
- %1$s Favorits
- - %s partatge
- - %s partatges
+ - %s Partatge
+ - %s Partatges
Partejat per
Aimat per
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index f2aa559d..e6573c15 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -20,7 +20,7 @@
Strona główna
Powiadomienia
Lokalne
- Globalne
+ Sfederowane
Wątek
Wpisy
Z odpowiedziami
@@ -33,12 +33,12 @@
Edytuj profil
Szkice
Licencje
- %s podbił
- Wrażliwe treści
- Ukryto zawartość multimedialną
+ %s podbite
+ Treści wrażliwe
+ Ukryto multimedia
Naciśnij, aby wyświetlić
Pokaż więcej
- Ukryj
+ Pokaż mniej
Pusto tutaj. Pociągnij, aby odświeżyć!
%s podbił(-a) Twój wpis
%s dodał Twój post do ulubionych
@@ -95,13 +95,13 @@
Klawiatura emoji
Pobieranie %1$s
Skopiuj odnośnik
- Udostępnij odnośnik do wpisu…
+ Udostępnij URL do…
Udostępnij wpis do…
Wyślij!
Odblokowano użytkownika
Cofnięto wyciszenie użytkownika
Wyślij!
- Pomyślnie wysłano odpowiedź.
+ Odpowiedź wysłano pomyślnie.
Jaka instancja?
Co Ci chodzi po głowie?
Ostrzeżenie o zawartości
@@ -151,7 +151,7 @@
Używaj niestandardowych kart Chrome
Ukryj przycisk śledzenia podczas przewijania
Filtrowanie osi czasu
- Zakładki
+ Karty
Pokaż podbicia
Pokazuj odpowiedzi
Pokazuj podgląd zawartości multimedialnej
@@ -183,10 +183,10 @@
%1$s, %2$s, i %3$s
%1$s i %2$s
- - %d nowe powiadomienie
- - %d nowe powiadomienia
- - %d nowych powiadomień
- - %d nowych powiadomień
+ - %d nowa interakcja
+ - %d nowe interakcje
+ - %d nowych interakcji
+ - %d nowych interakcji
Konto zablokowane
O programie
@@ -244,7 +244,6 @@
Kopia wpisu została zapisana jako szkic
Nowy wpis
Twoja instancja %s nie używa żadnych niestandardowych emoji
- Skopiowano do schowka
Styl emoji
Domyślny systemu
Musisz najpierw pobrać te zestawy emoji
@@ -405,25 +404,25 @@
Głosowanie w którym brałeś(-aś) udział zakończyła się
Ankieta, którą stworzyłeś(aś), zakończyła się
- - Zostało %d dzień
+ - Został %d dzień
- Zostało %d dni
- Zostało %d dni
- Zostało %d dni
- - Zostało %d godzina
+ - Została %d godzina
- Zostało %d godziny
- Zostało %d godzin
- Zostało %d godzin
- - Zostało %d minuta
+ - Została %d minuta
- Zostało %d minuty
- Zostało %d minut
- Zostało %d minut
- - Zostało %d sekunda
+ - Została %d sekunda
- Zostało %d sekund
- Zostało %d sekund
- Zostało %d sekund
@@ -463,7 +462,7 @@
Zakładki
Dodaj do zakładek
Zakładki
- Dodane do zakładek
+ Dodany do zakładek
Wybierz listę
Lista
Pliki audio muszą być mniejsze niż 40MB.
@@ -494,8 +493,8 @@
Dół
Góra
- - Nie możesz przesłać więcej niż %1$d załącznika.
- - Nie możesz przesłać więcej niż %1$d załączników.
+ - Nie możesz przesłać więcej niż %1$d załącznik.
+ - Nie możesz przesłać więcej niż %1$d załączniki.
- Nie możesz przesłać więcej niż %1$d załączników.
- Nie możesz przesłać więcej niż %1$d załączników.
@@ -516,21 +515,21 @@
Włącz gest przesuwania by przełączać między zakładkami
Załączniki
Powiadomienia o prośbach o obserwowanie
- ktoś kogo zasubskrybowałem/zasubskrybowałam opublikował nowy wpis
+ ktoś zasubskrybowany opublikował nowy wpis
Wysłano prośbę o obserwowanie
Ogłoszenia
- Samopoczucie
+ Zdrowie
Anuluj subskrypcję
Zasubskrybuj
Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont.
- Wpis dla którego naszkicowałeś/naszkicowałaś odpowiedź został usunięty
+ Wpis dla którego naszkicowałeś/aś odpowiedź został usunięty
Usunięto szkic
Ukryj ilościowe statystyki na profilach
Ukryj ilościowe statystyki na postach
Przejrzyj powiadomienia
Zapisano!
Twoja prywatna notatka o tym koncie
- Czas nieokreślony
+ Nieograniczony
Dźwięk
Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz
Pozycja głównego paska nawigacji
@@ -549,4 +548,15 @@
180 dni
365 dni
Utwórz wpis
+ Login
+ %s zarejestrował(a) się
+ Rejestracje
+ Powiadomienia o nowych użytkownikach
+ Powiadomienia o edycji wpisów z którymi dokonałeś/aś interakcji
+ ktoś zarejestrował się
+ wpis, z którym dokonałem/am interakcji został edytowany
+ %s edytował(a) swój wpis
+ Edycje wpisów
+ Zapisywanie szkicu…
+ Nie można załadować strony logowania.
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5f703152..e6af0372 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -261,7 +261,6 @@
Uma cópia do toot foi salva nos seus rascunhos
Compor
A sua instância %s não possui emojis personalizados
- Copiado para a área de transferência
Estilo de emoji
Padrão do sistema
É necessário baixar estes pacotes de emojis primeiro
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 00000000..6be06b0c
--- /dev/null
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,533 @@
+
+
+ Maior
+ Toots novos
+ Criação de contas
+ %1$s e %2$s
+
+ - %d nova interação
+ - %d novas interações
+
+ A responder a @%s
+ Editar a lista
+ Necessita de aprovar manualmente os seguidores
+ Guardar rascunho\?
+ Depois
+ Desafixar
+ %1$s e %2$s
+ Bem-estar
+ Escrever Toot
+ Pretende remover a lista %s\?
+ Apesar do seu perfil não ser privado, %1$s exige que você reveja manualmente as solicitações para te seguir destes perfis.
+ Subscrever
+ Remover subscrição
+ Autorização negada.
+ Erro ao adquirir token de login.
+ O toot é muito extenso!
+ O ficheiro deve ter menor de 8MB.
+ Os ficheiros de vídeo devem ter menor de 40MB.
+ Os ficheiros de áudio devem ter menor de 40MB.
+ Esse tipo de ficheiro não pode ser enviado.
+ Não foi possível abrir esse ficheiro.
+ É necessária permissão para ler o armazenamento.
+ É necessária permissão para escrever no armazenamento.
+ Não é possível anexar imagens e vídeos no mesmo toot.
+ Erro ao enviar.
+ Erro ao publicar o toot.
+ Página inicial
+ Notificações
+ Local
+ Federada
+ Mensagens Diretas
+ Separadores
+ Conversa
+ Toots
+ Com respostas
+ Fixado
+ Segue
+ Seguidores
+ Favoritos
+ Itens guardados
+ Utilizadores silenciados
+ Utilizadores bloqueados
+ Instâncias bloqueadas
+ Seguidores Pendentes
+ Conteúdo sensível
+ Editar perfil
+ Conteúdo multimédia ocultado
+ Rascunhos
+ Toque para ver
+ Mostrar Mais
+ Mostrar Menos
+ Expandir
+ Contrair
+ Toots agendados
+ Anúncios
+ Licenças
+ \@%s
+ %s fez boost
+ Nada aqui.
+ Nada para ver aqui. Arraste para baixo para atualizar!
+ %s fez boost ao seu toot
+ %s adicionou o seu toot aos favoritos
+ %s está a seguir-te
+ %s pediu para te seguir
+ %s criou conta
+ %s acabou de publicar um toot
+ %s editou um toot
+ Denunciar @%s
+ Comentários adicionais\?
+ Resposta Rápida
+ Responder
+ Fazer boost
+ Desfazer boost
+ Adicionar aos favoritos
+ Remover dos favoritos
+ Guardar
+ Remover dos itens guardados
+ Mais
+ Escrever
+ Entrar com Mastodon
+ Sair
+ Tem a certeza que deseja sair da conta %1$s\?
+ Seguir
+ Deixar de seguir
+ Bloquear
+ Desbloquear
+ Esconder boosts
+ Mostrar boosts
+ Denunciar
+ Editar
+ Apagar
+ Apagar conversa
+ Apagar e criar novo rascunho
+ TOOT
+ TOOT!
+ Tentar novamente
+ Fechar
+ Perfil
+ Configurações
+ Configurações da Conta
+ Favoritos
+ Itens Guardados
+ Utilizadores silenciados
+ Utilizadores bloqueados
+ Instâncias bloqueadas
+ Seguidores Pendentes
+ Conteúdo multimédia
+ Abrir no navegador
+ Adicionar conteúdo multimédia
+ Adicionar votação
+ Tirar foto
+ Partilhar
+ Silenciar
+ Remover silêncio
+ Remover %s do silêncio
+ Remover notificações de %s do silêncio
+ Silencie notificações de %s
+ Silenciar %s
+ Remover %s do silêncio
+ Silenciar conversa
+ Remover conversa do silêncio
+ Mencionar
+ Esconder conteúdo multimédia
+ Abrir menu
+ Pesquisar
+ Rascunhos
+ Toots agendados
+ Privacidade do toot
+ Aviso de conteúdo
+ Teclado de emojis
+ Agendar Toot
+ Redefinir
+ Adicionar Separador
+ Hiperligações
+ Menções
+ Hashtags
+ Ver autor do boost
+ Mostrar boosts
+ Mostrar favoritos
+ Hashtags
+ Menções
+ Hiperligações
+ Abrir conteúdo multimédia #%d
+ A descarregar %1$s
+ Copiar a hiperligação
+ Abrir como %s
+ Partilhar como…
+ Descarregar conteúdo multimédia
+ A descarregar conteúdo multimédia
+ Partilhar hiperligação do toot via…
+ Partilhar toot via…
+ Partilhar conteúdo multimédia via…
+ Enviado!
+ Utilizador desbloqueado
+ Utilizador removido do silêncio
+ %s desbloqueada
+ Enviado!
+ Resposta enviada com sucesso.
+ Que instância\?
+ Em que está a pensar\?
+ Aviso de conteúdo
+ Nome
+ Biografia
+ Pesquisar…
+ Sem resultados
+ Responder…
+ Avatar
+ Cabeçalho
+ O que é uma instância\?
+ A ligar…
+ O endereço IP ou domínio de qualquer instância pode ser inserido aqui, como por exemplo mastodon.social, masto.pt, pleroma.pt ou qualquer outro!
+\n
+\nSe ainda não tem uma conta, insira o nome da instância onde pretende participar e crie uma conta lá.
+\n
+\nUma instância é o local onde sua conta é criada, mas pode facilmente seguir e comunicar com pessoas de outras instâncias como se estivessem todos no mesmo site.
+\n
+\nMais informações disponíveis em joinmastodon.org.
+ A Terminar Envio de Conteúdo Multimédia
+ A enviar…
+ Descarregar
+ Cancelar o pedido para seguir\?
+ Deixar de seguir esta conta\?
+ Apagar este toot\?
+ Apagar e rescrever este toot\?
+ Apagar esta conversa\?
+ Tem a certeza que pretende bloquear a instância %s\? Deixará de poder ver quaisquer conteúdos dessa instância em qualquer timeline pública ou nas suas notificações. Os seus seguidores dessa instância serão removidos.
+ Bloquear instância
+ Bloquear @%s\?
+ Silenciar @%s\?
+ Esconder notificações
+ Público: Publicar em timelines públicas
+ Não listado: Não publicar em timelines públicas
+ Privado: Publicar apenas para os seguidores
+ Direto: Publicar apenas para os utilizadores mencionados
+ Notificações
+ Notificações
+ Alertas
+ Notificar com som
+ Notificar com vibração
+ Notificar com luz
+ Notifique-me quando
+ for mencionado
+ for seguido
+ alguém para quem ativei os alertas publicar um toot novo
+ fizerem pedido para me seguir
+ fizerem boosts aos meus toots
+ adicionarem os meus toots aos favoritos
+ votações terminarem
+ alguém criar conta
+ um toot com o qual interagi for editado
+ Aparência
+ Tema da Aplicação
+ Timelines
+ Filtros
+ Escuro
+ Claro
+ AMOLED
+ Automático ao pôr-do-sol
+ Usar o Tema do Sistema
+ Navegador
+ Usar Separadores Personalizados do Chrome
+ Esconder o botão de criação de toots ao fazer scroll
+ Idioma
+ Mostrar indicador para bots
+ Reproduzir avatars em GIF
+ Mostrar desfocagem em conteúdo multimédia sensível
+ Animar emojis personalizados
+ Filtro da timeline
+ Separadores
+ Mostrar boosts
+ Mostrar respostas
+ Mostrar pré-visualização de conteúdo multimédia
+ Proxy
+ Proxy HTTP
+ Ativar proxy HTTP
+ Servidor da proxy HTTP
+ Privacidade padrão dos toots
+ Classificar sempre conteúdo multimédia como sensível
+ Toots (sincronizados com a instância)
+ Erro ao sincronizar configurações
+ Posição do menu principal
+ Superior
+ Inferior
+ Público
+ Porta da proxy HTTP
+ Não listado
+ Privado
+ Menor
+ Pequeno
+ Médio
+ Grande
+ Menções Novas
+ Notificações para menções novas
+ Novos Seguidores
+ Tamanho do texto do toot
+ Notificações para seguidores novos
+ Seguidores Pendentes
+ Notificações para seguidores pendentes
+ Boosts
+ Votações
+ Notificações para votações terminadas
+ Notificações quando alguém para quem ativou os alertas publicar um toot novo
+ Notificações para novos utilizadores
+ Edições de toots
+ Notificações para boosts recebidos
+ Favoritos
+ Notificações quando os seus toots são adicionados aos favoritos
+ Notificações quando toots com os quais interagiu forem editados
+ %s mencionou-te
+ %1$s, %2$s, %3$s e %4$d outros
+ %1$s, %2$s e %3$s
+ Perfil Privado
+ Sobre
+ Tusky %s
+ A correr o Tusky
+ Atualizar
+ Tusky é um software livre e de código aberto, licenciado com a versão 3 da GNU General Public License. Pode ler a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html
+ Página do projeto:
+\n https://tusky.app
+ Reporte de erros e pedidos de funcionalidades:
+\n https://github.com/tuskyapp/Tusky/issues
+ Perfil do Tusky
+ Partilhar conteúdo do toot
+ Partilhar hiperligação do toot
+ Imagens
+ Vídeo
+ Áudio
+ Anexos
+ Pedido para seguir enviado
+ em %dy
+ em %dd
+ em %dh
+ em %dm
+ em %ds
+ %dy
+ %dd
+ %dh
+ %dm
+ %ds
+ Segue-te
+ Mostrar sempre conteúdo multimédia sensível
+ Expandir sempre toots com Aviso de Conteúdo
+ Palavra completa
+ Conteúdo Multimédia
+ carregar mais
+ Timelines públicas
+ Conversas
+ Criar filtro
+ Editar filtro
+ Remover
+ Se a palavra ou frase for alfanumérica, só será aplicado se corresponder à palavra completa
+ Frase para filtrar
+ Adicionar Conta
+ Adicionar nova Conta Mastodon
+ Listas
+ Não foi possível renomear a lista
+ Listas
+ Cronologia da timeline
+ Não foi possível criar a lista
+ Não foi possível apagar a lista
+ Criar uma lista
+ Renomear a lista
+ Apagar a lista
+ Pesquisar pessoas que você segue
+ Adicionar conta à lista
+ Remover conta da lista
+ Publicar com a conta %1$s
+ Erro ao incluir descrição
+
+ - Descrição para deficientes visuais
+\n(até %d letra)
+ - Descrição para deficientes visuais
+\n(até %d caracteres)
+
+ Escrever descrição
+ Remover
+ Bloquear perfil
+ A enviar o toot…
+ Erro ao enviar o toot
+ A Enviar os Toots
+ Envio cancelado
+ Uma cópia do toot foi guardada nos seus rascunhos
+ Escrever
+ A sua instância, %s, não tem emojis personalizados
+ Estilo dos emojis
+ Padrão do sistema
+ É necessário descarregar estes pacotes de emojis primeiro
+ A fazer pesquisa…
+ Expandir/Contrair todos os toots
+ Abrir toot
+ É necessário reiniciar a aplicação
+ É necessário reiniciar o Tusky para aplicar as alterações
+ Reiniciar
+ Pacote de emojis padrão do seu dispositivo
+ Emojis padrão do Android 4.4 até ao 7.1
+ Pacote de emojis padrão do Mastodon
+ Pacote de emojis atuais da Google
+ Erro ao descarregar
+ Robô
+ %1$s mudou-se para:
+ Dar boost para o público inicial
+ Desfazer boost
+ O Tusky contém código e recursos dos seguintes projetos de código aberto:
+ Licenciado sob a licença Apache (cópia abaixo)
+ CC-BY 4.0
+ CC-BY-SA 4.0
+ Metadados do perfil
+ adicionar dados
+ Rótulo
+ Conteúdo
+ Usar data absoluta
+ As informações abaixo podem refletir, de forma incompleta, o perfil do utilizador. Toque aqui para abrir o perfil completo no navegador.
+ Fixar
+
+ - %1$s Favorito
+ - %1$s Favoritos
+
+
+ - %s Boost
+ - %s Boosts
+
+ Boost dado por
+ Adicionado aos favoritos por
+ %1$s
+ %1$s, %2$s e %3$d mais
+
+ - atingiu o máximo de %1$d separador
+ - atingiu o máximo de %1$d separadores
+
+ Conteúdo multimédia: %s
+ Aviso de Conteúdo: %s
+ Sem descrição
+ Replicado
+ Adicionado aos favoritos
+ Guardado
+ Público
+ Não-listado
+ Privado
+ Direto
+ Votação com as opções: %1$s, %2$s, %3$s, %4$s; %5$s
+ Nome da lista
+ Adicionar hashtag
+ Hashtag sem #
+ Hashtags
+ Selecionar lista
+ Lista
+ Limpar
+ Filtrar
+ Aplicar
+ Escrever toot
+ Escrever
+ Tem certeza que pretende limpar permanentemente todas as suas notificações\?
+ Opções para imagem %s
+ %1$s • %2$s
+
+ - %s voto
+ - %s votos
+
+
+ - %s pessoa
+ - %s pessoas
+
+ termina em %s
+ terminada
+ Votar
+ Uma votação em que votou terminou
+ A sua votação terminou
+
+ - %d dia restante
+ - %d dias restantes
+
+
+ - %d hora restante
+ - %d horas restantes
+
+
+ - %d minuto restante
+ - %d minutos restantes
+
+
+ - %d segundo restante
+ - %d segundos restantes
+
+ Continuar
+ Retroceder
+ Feito
+ \@%s denunciado com sucesso
+ Comentários adicionais
+ Encaminhar para %s
+ Erro ao denunciar
+ Erro ao carregar toots
+ A denúncia será enviada aos moderadores da instância. Pode adicionar abaixo uma explicação para a sua denúncia:
+ A conta está noutra instância. Quer enviar uma cópia anónima da denúncia para lá\?
+ Contas
+ Erro ao pesquisar
+ Mostrar Filtro das Notificações
+ Ativar gesto de deslizar para alternar entre separadores
+ Votação
+ Duração
+ Indefinido
+ 5 minutos
+ 30 minutos
+ 1 hora
+ 6 horas
+ 1 dia
+ 3 dias
+ 7 dias
+ 14 dias
+ 30 dias
+ 60 dias
+ 90 dias
+ 180 dias
+ 365 dias
+ Adicionar opção
+ Escolha múltipla
+ Opção %d
+ Editar
+ Erro ao pesquisar toot %s
+ Não tem rascunhos.
+ Não tem toots agendados.
+ Guardado!
+ Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui:
+\n
+\n- Notificações de favoritos, boosts e seguidores
+\n- Número de favoritos e boosts nos toots
+\n- Status de toots e seguidores nos perfis
+\n
+\nNotificações push não serão afetadas, mas é possível rever as configurações das notificações manualmente.
+ Rever Notificações
+ Limitar notificações da timeline
+ Sem anúncios.
+ O Mastodon tem um intervalo mínimo de agendamento de 5 minutos.
+ Mostrar pré-visualização de hiperligações nas timelines
+ Mostrar janela de confirmação antes de dar boost
+ Mostrar janela de confirmação antes de adicionar aos favoritos
+ Esconder o título da barra superior
+ Nota pessoal sobre este perfil
+ Esconder estatísticas quantitativas nos toots
+ Esconder estatísticas quantitativas nos perfis
+
+ - Não é possível enviar mais de %1$d arquivo de conteúdo multimédia.
+ - Não é possível enviar mais de %1$d arquivos de conteúdo multimédia.
+
+ Erro ao enviar o toot!
+ Erro ao carregar informação de resposta
+ Rascunho apagado
+ O toot para o qual escreveu um rascunho foi apagado
+ Ocorreu um erro.
+ Ocorreu um erro de conetividade! Por favor, verifique a sua ligação e tente novamente!
+ Isto não pode estar vazio.
+ A instância inserida é inválida
+ Erro ao autenticar com esta instância.
+ Não foi possível encontrar um navegador.
+ Ocorreu um erro de autorização não identificado.
+ Entrar
+ Guardar
+ Editar perfil
+ Editar
+ Desfazer
+ Aceitar
+ Rejeitar
+ Não foi possível carregar a página de login
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 184497b8..67af8cc7 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -324,7 +324,6 @@
Копия поста сохранена в ваши черновики
Сочинить
У вашего узла %s нет собственных эмодзи
- Скопировано в буфер обмена
Стиль эмодзи
Системный
Сперва эти наборы эмодзи нужно скачать
diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml
index bb411871..4c8ac044 100644
--- a/app/src/main/res/values-sa/strings.xml
+++ b/app/src/main/res/values-sa/strings.xml
@@ -317,7 +317,6 @@
प्राक्तु भावचिह्नसमूहोऽयमवारोप्यः
प्रणाल्यां पूर्वनिविष्टम्
भावचिह्नशैली
- अंशफलकेऽनुसृतम्
भवदीयं विशिष्टस्थलं %s स्वीयानुकूलभावचिह्नरहितं वर्तते
लिख्यताम्
दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index df654a29..7ba70498 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -171,7 +171,6 @@
\n https://tusky.app
පිළිගන්න
පැ. %d කින්
- පසුරුපුවරුවට පිටපත් විය
මතවිමසුම
ඉවත් කරන්න
මාධ්ය එකතු කරන්න
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 15d05945..e06555c7 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -77,7 +77,7 @@
Autentizácia servru zlyhala.
Nepodarilo sa nájsť použiteľný webový prehliadač.
Vyskytla sa neidentifikovaná chyba autorizácie.
- Toot je príliš dlhý!
+ Príspevok je príliš dlhý!
Tento typ súboru nemôže byť nahraný.
Chyba pri odosielaní tootu.
Toot
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 4919b3ec..f6d6caad 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -276,7 +276,6 @@
Kopija tuta je bila shranjena v osnutke
Sestavi
Vaše vozlišče %s nima emotikonov po meri
- Kopirano v odložišče
Slog emotikonov
Privzete nastavitve sistema
Najprej boste morali prenesti te emotikone
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 1bbc64a8..f9908d87 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -297,7 +297,6 @@
En kopia av tooten har sparats i dina utkast
Skriv
Din instans %s har inga anpassade emojis
- Kopierat till urklipp
Emojis
Systemstandard
Du behöver ladda ned dessa emojis först
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 6625a19e..f199adf4 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -231,7 +231,6 @@
நகலெடுக்கபட்ட toot வரைவில் சேமிக்கபட்டது
எழுது
தங்கள் %s instance(களம்)-ல் எந்தவொரு custom emojis-ம் இல்லை
- பிடிப்புப்பலகையில் நகலெடுக்க
Emoji பாணி
அமைப்பின் இயல்புநிலை
தாங்கள் முதலில் இந்த Emoji sets-னை பதிவிறக்கவேண்டும்
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index fa27fbe0..781a432b 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -116,7 +116,6 @@
ต้องดาวน์โหลดชุดเอโมจิเหล่านี้ก่อน
ค่าปริยายของระบบ
รูปแบบเอโมจิ
- คัดลอกไปยังคลิบบอร์ดแล้ว
Instance %s ไม่มีเอโมจิแบบกำหนดเอง
เขียน
สำเนา Toot บันทึกเป็นฉบับร่างแล้ว
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 25a5efcb..dea620be 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -261,7 +261,6 @@
Tootun bir kopyası taslaklara kaydedildi
Oluştur
%s örneğinizin herhangi bir özel ifadesi yok
- Panoya kopyalandı
İfade stili
Sistem varsayılanı
Önce bu ifade paketini indirmeniz gerekecek
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 0031334b..5998c9b0 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -100,7 +100,7 @@
%s надсилає запит на підписку
%s підписується на вас
Тут нічого немає. Потягніть вниз, щоб оновити!
- Тут нічого немає.
+ Тут порожньо.
Згорнути
Розгорнути
Натисніть для перегляду
@@ -181,7 +181,6 @@
Спочатку потрібно буде завантажити ці набори емодзі
Типовий системний
Стиль емодзі
- Скопійовано до буфера обміну
Ваш сервер %s не має власних емодзі
Зберегти чернетку\?
Вимагає затвердження підписників власноруч
@@ -541,4 +540,15 @@
180 днів
365 днів
Створити допис
+ %s реєструється
+ хтось реєструється
+ Реєстрації
+ Сповіщення про нових користувачів
+ %s редагує свій допис
+ допис, з яким у мене була взаємодія, відредаговано
+ Сповіщення, коли редагується повідомлення, з яким ви взаємодіяли
+ Редакції допису
+ Вхід
+ Не вдалося завантажити сторінку входу.
+ Збереження чернетки…
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index d228c6be..125e452e 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -192,7 +192,7 @@
Ghim
Trả lời
Tút
- Nội dung tút
+ Tút
Xếp tab
Tin nhắn
Thế giới
@@ -413,7 +413,7 @@
Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:
Hủy đăng lại
Đăng lại công khai
- %1$s đã dời sang:
+ %1$s đã chuyển sang:
Tài khoản Bot
Tải về thất bại
Emoji của Google
@@ -430,7 +430,6 @@
Bạn cần tải về bộ emoji này trước
Mặc định của thiết bị
Emoji
- Đã chép vào clipboard
Viết
Lưu nháp\?
Tự bạn sẽ phê duyệt người theo dõi
@@ -508,4 +507,15 @@
180 ngày
365 ngày
Viết tút
+ ai đó đăng ký trên máy chủ
+ %s đăng ký
+ Đăng ký
+ Thông báo về người dùng mới đăng ký
+ %s đã sửa tút của họ
+ khi một tút mà tôi tương tác bị sửa
+ Sửa tút
+ Thông báo khi tút mà tôi tương tác bị sửa
+ Đăng nhập
+ Không thể tải trang đăng nhập.
+ Đang lưu nháp…
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 0866fef3..2e6a8042 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -27,7 +27,7 @@
标签页
嘟文
嘟文
- 嘟文和回复
+ 有回复
已置顶
正在关注
关注者
@@ -42,7 +42,7 @@
%s 转嘟了
敏感内容
已隐藏的照片或视频
- 点击显示
+ 点击查看
显示更多
折叠内容
展开
@@ -156,7 +156,7 @@
移除关注请求?
不再关注此用户?
删除这条嘟文?
- 删除并重新编辑这条嘟文?
+ 删除并重新起草这条嘟文?
公开:所有人可见,并会出现在公共时间轴上
不公开:所有人可见,但不会出现在公共时间轴上
仅关注者:只有经过你确认后关注你的用户可见
@@ -203,7 +203,7 @@
公开
不公开
仅关注者
- 字体大小
+ 嘟文字体大小
最小
小
标准
@@ -299,14 +299,13 @@
保护你的帐户(锁嘟)
你需要手动审核所有关注请求
保存为草稿?
- 正在发送…
- 发送失败
+ 正在发送嘟文…
+ 嘟文发送出错
嘟文发送中
已取消发送
- 嘟文已保存为草稿
+ 嘟文副本已保存为草稿
发表嘟文
当前实例 %s 没有自定义表情符号
- 已复制到剪贴板
表情符号风格
系统默认
需要下载表情符号数据
@@ -353,16 +352,10 @@
- 标签页不能超过 %1$d 个
媒体:%s
- 内容提醒:%s
-
- 没有媒体描述信息
-
-
- 被转嘟
-
-
- 被收藏
-
+ 内容警告:%s
+ 没有描述信息
+ 被转嘟
+ 被收藏
公开
@@ -435,7 +428,7 @@
附加留言
转发到 %s
举报失败
- 无法获取状态
+ 无法获取嘟文
该报告将发送给给您的服务器管理员。您可以在下面提供有关回报此帐户的原因的说明:
该帐户来自其他服务器。向那里发送一份匿名的报告副本?
账户
@@ -533,4 +526,15 @@
14 天
365 天
撰写嘟文
+ %s 已注册
+ 某人进行了注册
+ 新用户通知
+ 注册
+ 登录
+ %s 编辑了他们的嘟文
+ 我进行过互动的嘟文被编辑了
+ 嘟文编辑
+ 当你进行过互动的嘟文被编辑时发出通知
+ 无法加载登录页。
+ 正在保存草稿…
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 2877900a..5ac0003a 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -304,7 +304,6 @@
嘟文已儲存為草稿
新嘟文
當前站點 %s 沒有自訂表情符號
- 已複製到剪貼簿
表情符號風格
系統預設
你需要先下載這些表情符號包
diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml
index 49f1cb3f..4ea493d6 100644
--- a/app/src/main/res/values-zh-rMO/strings.xml
+++ b/app/src/main/res/values-zh-rMO/strings.xml
@@ -298,7 +298,6 @@
嘟文已儲存為草稿
發表新嘟文
當前站點 %s 沒有自訂表情符號
- 已複製到剪貼簿
表情符號風格
系統預設
你需要先下載這些表情符號包
diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml
index 66c0a842..dc729241 100644
--- a/app/src/main/res/values-zh-rSG/strings.xml
+++ b/app/src/main/res/values-zh-rSG/strings.xml
@@ -302,7 +302,6 @@
嘟文已保存为草稿
新嘟文
当前实例 %s 没有自定义表情符号
- 已复制到剪贴板
表情符号风格
系统默认
需要下载表情符号数据
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index e972d3a1..b5776edd 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -304,7 +304,6 @@
嘟文已儲存為草稿
發表新嘟文
當前站點 %s 沒有自訂表情符號
- 已複製到剪貼簿
表情符號風格
系統預設
你需要先下載這些表情符號包
@@ -526,4 +525,6 @@
總是顯示被標注為內容警告的嘟文
搜尋失敗
帳號
+ 登入
+ 無法載入登入頁面。
\ No newline at end of file
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index 9545b0dd..86dfb26a 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -57,6 +57,7 @@
- Occitan
- Polski
- Português (Brasil)
+ - Português (Portugal)
- Slovenščina
- Svenska
- Taqbaylit
@@ -106,6 +107,7 @@
- oc
- pl
- pt-BR
+ - pt-PT
- sl
- sv
- kab
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3cc43d4e..44160f62 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9,6 +9,7 @@
An unidentified authorization error occurred.
Authorization was denied.
Failed getting a login token.
+ Could not load the login page.
The post is too long!
The file must be less than 8MB.
Video files must be less than 40MB.
@@ -21,6 +22,7 @@
The upload failed.
Error sending post.
+ Login
Home
Notifications
Local
@@ -62,7 +64,9 @@
%s favorited your post
%s followed you
%s requested to follow you
+ %s signed up
%s just posted
+ %s edited their post
Report @%s
Additional comments?
@@ -228,6 +232,8 @@
my posts are favorited
polls have ended
somebody I\'m subscribed to published a new post
+ somebody signed up
+ a post I\'ve interacted with is edited
Appearance
App Theme
Timelines
@@ -295,6 +301,10 @@
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
+ Post edits
+ Notifications when posts you\'ve interacted with are edited
%s mentioned you
%1$s, %2$s, %3$s and %4$d others
@@ -403,7 +413,6 @@
Compose
Your instance %s does not have any custom emojis
- Copied to clipboard
Emoji style
System default
You\'ll need to download these emoji sets first
@@ -632,5 +641,6 @@
Register New Account
Compose Post
+ Saving draft…
diff --git a/app/src/main/res/values/theme_colors.xml b/app/src/main/res/values/theme_colors.xml
index bab199ea..cbc8d33d 100644
--- a/app/src/main/res/values/theme_colors.xml
+++ b/app/src/main/res/values/theme_colors.xml
@@ -24,4 +24,7 @@
true
+ @color/tusky_grey_20
+ @color/white
+
\ No newline at end of file
diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
index ef6d2632..beb6af9b 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,
@@ -306,7 +301,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 e7b3a1a9..3a8f2f23 100644
--- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
@@ -17,38 +17,32 @@ 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.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
+import com.keylesspalace.tusky.db.EmojisEntity
import com.keylesspalace.tusky.db.InstanceDao
-import com.keylesspalace.tusky.db.InstanceEntity
+import com.keylesspalace.tusky.db.InstanceInfoEntity
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
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,49 +88,55 @@ 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 {
+ onBlocking { getCustomEmojis() } doReturn Result.success(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 {
+ onBlocking { getInstanceInfo(any()) } doReturn
+ InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null)
+ onBlocking { getEmojiInfo(any()) } doReturn
+ EmojisEntity(instanceDomain, emptyList())
+ }
- val dbMock = mock(AppDatabase::class.java)
- `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
+ val dbMock: AppDatabase = mock {
+ on { instanceDao() } doReturn instanceDaoMock
+ }
+
+ val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock)
val viewModel = ComposeViewModel(
apiMock,
accountManagerMock,
- mock(MediaUploader::class.java),
- mock(ServiceClient::class.java),
- mock(DraftHelper::class.java),
- dbMock
+ mock(),
+ mock(),
+ mock(),
+ instanceInfoRepo
)
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
controller.create().start()
+ shadowOf(getMainLooper()).idle()
}
@Test
@@ -187,7 +187,7 @@ class ComposeActivityTest {
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
instanceResponseCallback = { getInstanceWithCustomConfiguration(null) }
setupActivity()
- assertEquals(DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
+ assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
}
@Test
@@ -238,7 +238,7 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = "Check out this @image #search result: "
insertSomeTextInContent(additionalContent + url)
- assertEquals(activity.calculateTextLength(), additionalContent.length + DEFAULT_MAXIMUM_URL_LENGTH)
+ assertEquals(activity.calculateTextLength(), additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL)
}
@Test
@@ -247,7 +247,7 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: "
insertSomeTextInContent(shortUrl + additionalContent + url)
- assertEquals(activity.calculateTextLength(), additionalContent.length + (DEFAULT_MAXIMUM_URL_LENGTH * 2))
+ assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
}
@Test
@@ -255,7 +255,7 @@ class ComposeActivityTest {
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
val additionalContent = " Check out this @image #search result: "
insertSomeTextInContent(url + additionalContent + url)
- assertEquals(activity.calculateTextLength(), additionalContent.length + (DEFAULT_MAXIMUM_URL_LENGTH * 2))
+ assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
}
@Test
@@ -470,7 +470,7 @@ class ComposeActivityTest {
"admin",
"admin",
"admin",
- SpannedString(""),
+ "",
"https://example.token",
"",
"",
@@ -490,7 +490,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 03fff5ee..91ea38d3 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 ed06e27c..3086036a 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/TuskyApplication.kt b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
index 7724ba76..9598f2c1 100644
--- a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -18,16 +18,16 @@ package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
-import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager
-import de.c1710.filemojicompat.FileEmojiCompatConfig
+import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
+import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() {
override fun onCreate() {
super.onCreate()
- EmojiCompat.init(FileEmojiCompatConfig(this, ""))
+ EmojiPackHelper.init(this, DefaultEmojiPackList.get(this))
}
override fun attachBaseContext(base: Context) {
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 462b0a4a..2778f8c2 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 2e67c6fe..33215e67 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 74d0fe25..eabf744c 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 f7c998b5..cc6a90bd 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,
@@ -50,7 +49,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete(
status = mockStatus(id),
isExpanded = false,
isShowingContent = false,
- isCollapsible = false,
isCollapsed = true,
)
diff --git a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
index 889e5f98..ed652418 100644
--- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
@@ -369,13 +369,36 @@ class TimelineDaoTest {
assertEquals("99", timelineDao.getTopPlaceholderId(1))
}
+ @Test
+ fun `preview card survives roundtrip`() = runBlocking {
+ val setOne = makeStatus(statusId = 3, cardUrl = "https://foo.bar")
+
+ for ((status, author, reblogger) in listOf(setOne)) {
+ timelineDao.insertAccount(author)
+ reblogger?.let {
+ timelineDao.insertAccount(it)
+ }
+ timelineDao.insertStatus(status)
+ }
+
+ val pagingSource = timelineDao.getStatuses(setOne.first.timelineUserId)
+
+ val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false))
+
+ val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
+
+ assertEquals(1, loadedStatuses.size)
+ assertStatuses(listOf(setOne), loadedStatuses)
+ }
+
private fun makeStatus(
accountId: Long = 1,
statusId: Long = 10,
reblog: Boolean = false,
createdAt: Long = statusId,
authorServerId: String = "20",
- domain: String = "mastodon.example"
+ domain: String = "mastodon.example",
+ cardUrl: String? = null,
): Triple {
val author = TimelineAccountEntity(
serverId = authorServerId,
@@ -403,6 +426,10 @@ class TimelineDaoTest {
)
} else null
+ val card = when (cardUrl) {
+ null -> null
+ else -> "{ url: \"$cardUrl\" }"
+ }
val even = accountId % 2 == 0L
val status = TimelineStatusEntity(
serverId = statusId.toString(),
@@ -433,7 +460,8 @@ class TimelineDaoTest {
expanded = false,
contentCollapsed = false,
contentShowing = true,
- pinned = false
+ pinned = false,
+ card = card,
)
return Triple(status, author, reblogAuthor)
}
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt
new file mode 100644
index 00000000..57f3bed4
--- /dev/null
+++ b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt
@@ -0,0 +1,46 @@
+package com.keylesspalace.tusky.util
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.time.Instant
+import java.util.Date
+import java.util.TimeZone
+
+class AbsoluteTimeFormatterTest {
+
+ private val formatter = AbsoluteTimeFormatter(TimeZone.getTimeZone("UTC"))
+ private val now = Date.from(Instant.parse("2022-04-11T00:00:00.00Z"))
+
+ @Test
+ fun `null handling`() {
+ assertEquals("??", formatter.format(null, true, now))
+ assertEquals("??", formatter.format(null, false, now))
+ }
+
+ @Test
+ fun `same day formatting`() {
+ val tenTen = Date.from(Instant.parse("2022-04-11T10:10:00.00Z"))
+ assertEquals("10:10", formatter.format(tenTen, true, now))
+ assertEquals("10:10", formatter.format(tenTen, false, now))
+ }
+
+ @Test
+ fun `same year formatting`() {
+ val nextDay = Date.from(Instant.parse("2022-04-12T00:10:00.00Z"))
+ assertEquals("04-12 00:10", formatter.format(nextDay, true, now))
+ assertEquals("04-12 00:10", formatter.format(nextDay, false, now))
+ val endOfYear = Date.from(Instant.parse("2022-12-31T23:59:00.00Z"))
+ assertEquals("12-31 23:59", formatter.format(endOfYear, true, now))
+ assertEquals("12-31 23:59", formatter.format(endOfYear, false, now))
+ }
+
+ @Test
+ fun `other year formatting`() {
+ val firstDayNextYear = Date.from(Instant.parse("2023-01-01T00:00:00.00Z"))
+ assertEquals("2023-01-01", formatter.format(firstDayNextYear, true, now))
+ assertEquals("2023-01-01 00:00", formatter.format(firstDayNextYear, false, now))
+ val inTenYears = Date.from(Instant.parse("2032-04-11T10:10:00.00Z"))
+ assertEquals("2032-04-11", formatter.format(inTenYears, true, now))
+ assertEquals("2032-04-11 10:10", formatter.format(inTenYears, false, now))
+ }
+}
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt
deleted file mode 100644
index 5dd5ea84..00000000
--- a/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.keylesspalace.tusky.util
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class EmojiCompatFontTest {
-
- @Test
- fun testCompareVersions() {
-
- assertEquals(
- -1,
- EmojiCompatFont.compareVersions(
- listOf(0),
- listOf(1, 2, 3)
- )
- )
- assertEquals(
- 1,
- EmojiCompatFont.compareVersions(
- listOf(1, 2, 3),
- listOf(0, 0, 0)
- )
- )
- assertEquals(
- -1,
- EmojiCompatFont.compareVersions(
- listOf(1, 0, 1),
- listOf(1, 1, 0)
- )
- )
- assertEquals(
- 0,
- EmojiCompatFont.compareVersions(
- listOf(4, 5, 6),
- listOf(4, 5, 6)
- )
- )
- assertEquals(
- 0,
- EmojiCompatFont.compareVersions(
- listOf(0, 0),
- listOf(0)
- )
- )
- }
-}
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt
deleted file mode 100644
index 2731228a..00000000
--- a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.keylesspalace.tusky.util
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-class VersionUtilsTest(
- private val versionString: String,
- private val supportsScheduledToots: Boolean
-) {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters
- fun data() = listOf(
- arrayOf("2.0.0", false),
- arrayOf("2a9a0", false),
- arrayOf("1.0", false),
- arrayOf("error", false),
- arrayOf("", false),
- arrayOf("2.6.9", false),
- arrayOf("2.7.0", true),
- arrayOf("2.00008.0", true),
- arrayOf("2.7.2 (compatible; Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site)", true),
- arrayOf("3.0.1", true)
- )
- }
-
- @Test
- fun testVersionUtils() {
- assertEquals(VersionUtils(versionString).supportsScheduledToots(), supportsScheduledToots)
- }
-}
diff --git a/build.gradle b/build.gradle
index c9311701..3a5251fa 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/de/changelogs/89.txt b/fastlane/metadata/android/de/changelogs/89.txt
new file mode 100644
index 00000000..cb92453b
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- "Öffnen als..." ist jetzt im Menü in Konto Profilen auch verfügbar, wenn mehrere Konten genutzt werden
+- Die Anmeldung wird jetzt über die WebView innerhalb der App abgewickelt
+- Unterstützung für Android 12
+- Unterstützung für die neue Mastodon instance configuration API
+- und einige andere kleine Fehlerbehebungen und Verbesserungen
diff --git a/fastlane/metadata/android/en-US/changelogs/91.txt b/fastlane/metadata/android/en-US/changelogs/91.txt
new file mode 100644
index 00000000..e1d98303
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- Support for new Mastodon 3.5 notification types
+- The bot badge now looks better and adjusts to the selected theme
+- Text can now be selected on the post detail view
+- Fixed a lot of bugs, including one that prevented logins on Android 6 and lower
diff --git a/fastlane/metadata/android/fr/changelogs/89.txt b/fastlane/metadata/android/fr/changelogs/89.txt
new file mode 100644
index 00000000..9c8ae3b9
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- L'option « Ouvrir comme… » disponible quand plusieurs comptes sont connectés est maintenant aussi accessible depuis le menu sur les profils
+- L'identification se fait maintenant par une WebView dans l'application
+- Android 12 est pris en charge
+- La nouvelle API Mastodon de configuration d'instance est prise en charge
+- et beaucoup d'autres petites corrections et améliorations
diff --git a/fastlane/metadata/android/hu/changelogs/89.txt b/fastlane/metadata/android/hu/changelogs/89.txt
new file mode 100644
index 00000000..e7803769
--- /dev/null
+++ b/fastlane/metadata/android/hu/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- "Megnyit, mint..." már a fiókok profiljainak menüjében is elérhető, amikor több fiókot használsz
+- A bejelentkezés az appon belül már WebView-ban működik
+- Android 12 támogatása
+- új Mastodon szerverkonfigurációs API támogatása
+- sok más kisebb javítás és fejlesztés
diff --git a/fastlane/metadata/android/is/changelogs/89.txt b/fastlane/metadata/android/is/changelogs/89.txt
new file mode 100644
index 00000000..e379fc72
--- /dev/null
+++ b/fastlane/metadata/android/is/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky útg.17.0
+
+- "Opna sem..." er núna líka á valmyndinni í notendasniðum þegar verið er að nota marga aðganga
+- Innskráning er núna meðhöndluð í WebView innan forritsins
+- Stuðningur við Android 12
+- Stuðningur við API-kerfisviðmót fyrir nýja uppsetningu Mastodon-tilvika
+- og mökkur af smærri endurbótum og lagfæringum
diff --git a/fastlane/metadata/android/pl/changelogs/89.txt b/fastlane/metadata/android/pl/changelogs/89.txt
new file mode 100644
index 00000000..edfc6f0a
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- "Otwórz jako..." teraz jest także dostępne w menu na profilach kont gdy używane jest kilka kont
+- Login teraz jest obsługiwany w WebView w aplikacji
+- Wsparcie dla Androida 12
+- Wsparcie nowego API konfiguracji instancji Mastodon
+- i wiele innych małych poprawek i ulepszeń
diff --git a/fastlane/metadata/android/pt-PT/changelogs/58.txt b/fastlane/metadata/android/pt-PT/changelogs/58.txt
new file mode 100644
index 00000000..24aad2f2
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/58.txt
@@ -0,0 +1,12 @@
+Tusky v6.0
+
+- Os filtros de timeline passaram para "Preferências da Conta" e sincronizam com servidor
+- Pode ter uma hashtag personalizada como separador
+- Suporte a edição de listas
+- O editor sugere emojis personalizados ao escrever
+- Nova configuração: "seguir tema do sistema"
+- Melhor acessibilidade da timeline
+- O Tusky ignora notificações desconhecidas, deixando de crashar
+- Nova opção: trocar o idioma do sistema por outro
+- Novas traduções
+- Muitas outras melhorias e correções
diff --git a/fastlane/metadata/android/pt-PT/changelogs/61.txt b/fastlane/metadata/android/pt-PT/changelogs/61.txt
new file mode 100644
index 00000000..3cc7097e
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/61.txt
@@ -0,0 +1,7 @@
+Tusky v7.0
+
+- Suporte para mostragem de votações, para votação e notificação de votações
+- Botões novos para filtrar notificações e excluí-las
+- Exclua e rascunhe os seus toots
+- Novo indicador que mostra, na foto de perfil, se uma conta é um bot (pode ser desativado nas preferências)
+- Novas traduções: Norueguês, Bokmål e Esloveno.
diff --git a/fastlane/metadata/android/pt-PT/changelogs/67.txt b/fastlane/metadata/android/pt-PT/changelogs/67.txt
new file mode 100644
index 00000000..5d3d3849
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/67.txt
@@ -0,0 +1,9 @@
+Tusky v9.0
+
+- Agora pode criar votações no Tusky
+- Pesquisa melhorada
+- Nova opção em "Preferências da Conta": "Expandir sempre os toots com Aviso de Conteúdo"
+- Avatars em formato quadrado com cantos arredondados
+- Agora é possível denunciar utilizadores, mesmo que não tenham toots
+- O Tusky vai recusar a ligação através de ligações simples (não encriptadas) em Android 6+
+- Muitas outras pequenas melhorias e correções de bugs
diff --git a/fastlane/metadata/android/pt-PT/changelogs/68.txt b/fastlane/metadata/android/pt-PT/changelogs/68.txt
new file mode 100644
index 00000000..91792113
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/68.txt
@@ -0,0 +1,3 @@
+Tusky v9.1
+
+Esta atualização garante compatibilidade com Mastodon 3 e melhora a performance e estabilidade.
diff --git a/fastlane/metadata/android/pt-PT/changelogs/70.txt b/fastlane/metadata/android/pt-PT/changelogs/70.txt
new file mode 100644
index 00000000..6ac528b1
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/70.txt
@@ -0,0 +1,8 @@
+Tusky v10.0
+
+- Agora é possível adicionar toots aos favoritos e ver a lista de favoritos no Tusky.
+- Já pode agendar toots, no entanto é necessário agendá-los para pelo menos 5 minutos depois do momento da escrita.
+- Já pode adicionar listas na barra lateral do Tusky!
+- Já pode partilhar ficheiros de som nos teus toots!
+
+E muitas outras pequenas melhorias e correções de bugs!
diff --git a/fastlane/metadata/android/pt-PT/changelogs/72.txt b/fastlane/metadata/android/pt-PT/changelogs/72.txt
new file mode 100644
index 00000000..f42b0a8e
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/72.txt
@@ -0,0 +1,11 @@
+Tusky v11.0
+
+- Notificações de seguidores pendentes quando a conta está trancada!
+- Novas funcionalidades nas "Preferências":
+ * desativação do gesto que alterna entre separadores
+ * diálogo de confirmação antes de dar boost
+ * mostragem da pré-visualização de links nas timelines
+- As conversas agora podem ser silenciadas
+- As votações passam a ser calculadas pelo número de votantes e não pelo número de votos
+- Várias correções relacionadas com a escrita de toots
+ - Traduções melhoradas
diff --git a/fastlane/metadata/android/pt-PT/changelogs/74.txt b/fastlane/metadata/android/pt-PT/changelogs/74.txt
new file mode 100644
index 00000000..9595cb1f
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/74.txt
@@ -0,0 +1,8 @@
+Tusky v.12.0
+
+- Interface principal melhorada - passa a ser possível mover os separadores para baixo!
+- Ao silenciar um utilizador, pode também escolher se também pretende silenciar as notificações
+- Agora dá para seguir quantas hashtags quiser num único separador!
+- A exibição da descrição dos conteúdos multimédia foi melhorada para suportar descrições super longas
+
+Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/pt-PT/changelogs/77.txt b/fastlane/metadata/android/pt-PT/changelogs/77.txt
new file mode 100644
index 00000000..01c57b33
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/77.txt
@@ -0,0 +1,10 @@
+Tusky v13.0
+
+- Suporte para anotações em perfis (novidade do Mastodon 3.2.0)
+- Suporte para anúncios do(s) administrador(es) de instâncias (novidade do Mastodon 3.1.0)
+
+- O avatar da sua conta selecionada passa a ficar visível na barra de ferramentas principal (canto superior esquerdo)
+- Tocar no nome de utilizador na timeline abrirá o perfil em questão
+
+- Várias pequenas melhorias e correções
+- Traduções melhoradas
diff --git a/fastlane/metadata/android/pt-PT/changelogs/80.txt b/fastlane/metadata/android/pt-PT/changelogs/80.txt
new file mode 100644
index 00000000..866dc8e5
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/80.txt
@@ -0,0 +1,7 @@
+Tusky v14.0
+
+- Receba notificações quando um utilizador que segue publicar um toot - basta clicar no ícone do sino (novidade do Mastodon 3.3.0)
+- O suporte para rascunhos do Tusky foi reescrito para ser mais rápido, simples e menos propenso a erros.
+- Foi adicionado uma funcionalidade de bem-estar, que permite limitar algumas funcionalidades no Tusky.
+- O Tusky já consegue animar os emojis personalizados
+Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/pt-PT/changelogs/82.txt b/fastlane/metadata/android/pt-PT/changelogs/82.txt
new file mode 100644
index 00000000..0ee9e876
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/82.txt
@@ -0,0 +1,5 @@
+Tusky v15.0
+
+- O menu principal passa a mostrar uma opção para ver os utilizadores que pediram para o seguir!
+- O relógio para agendar toots ganhou um aspeto mais consistente com o resto do Tusky
+Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/pt-PT/changelogs/83.txt b/fastlane/metadata/android/pt-PT/changelogs/83.txt
new file mode 100644
index 00000000..4c71e64d
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/83.txt
@@ -0,0 +1,3 @@
+Tusky v15.1
+
+O Tusky já não crasha ao adicionar descrição às imagens
diff --git a/fastlane/metadata/android/pt-PT/changelogs/87.txt b/fastlane/metadata/android/pt-PT/changelogs/87.txt
new file mode 100644
index 00000000..79a81157
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky v16.0
+
+- O algoritmo de carregamento da timeline foi completamente reescrito para ser mais rápida, mais estável e mais fácil de manter.
+- O Tusky passa a poder animar emojis personalizados no formato APNG & WebP Animated.
+- Muitas correções de bugs
+- Suporte para Android 11
+- Novas traduções: gaélico escocês, galego, ucraniano
+- Traduções melhoradas
diff --git a/fastlane/metadata/android/pt-PT/changelogs/89.txt b/fastlane/metadata/android/pt-PT/changelogs/89.txt
new file mode 100644
index 00000000..28cebc1b
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- "Abrir como..." está disponível no menu de perfis de contas quando estão várias contas configuradas
+- O login passa a ser feito numa WebView dentro da aplicação
+- Suporte para Android 12
+- Suporte para a nova API de configuração de instâncias do Mastodon
+- Várias pequenas melhorias e correções
diff --git a/fastlane/metadata/android/pt-PT/full_description.txt b/fastlane/metadata/android/pt-PT/full_description.txt
new file mode 100644
index 00000000..52d67d81
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/full_description.txt
@@ -0,0 +1,12 @@
+Tusky é um cliente leve para Mastodon, um servidor de rede social de código aberto e livre.
+
+• Design Material
+• Maioria das APIs do Mastodon implementadas
+• Suporte para várias contas
+• Temas diurno e noturno, com possibilidade de troca automática de acordo com o horário
+• Rascunhos - Escreva os seus toots e guarde-os para mais tarde
+• Escolha entre estilos diferentes de emoji
+• Otimizado para todos os tamanhos de ecrã
+• Código totalmente aberto, sem dependências não-livres como Google Play Services
+
+Para ler mais sobre o Mastodon, visite o endereço https://joinmastodon.org/
diff --git a/fastlane/metadata/android/pt-PT/short_description.txt b/fastlane/metadata/android/pt-PT/short_description.txt
new file mode 100644
index 00000000..38a439d8
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/short_description.txt
@@ -0,0 +1 @@
+Um cliente multi-contas para a rede social Mastodon
diff --git a/fastlane/metadata/android/pt-PT/title.txt b/fastlane/metadata/android/pt-PT/title.txt
new file mode 100644
index 00000000..0238ffc0
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/title.txt
@@ -0,0 +1 @@
+Tusky
diff --git a/fastlane/metadata/android/uk/changelogs/91.txt b/fastlane/metadata/android/uk/changelogs/91.txt
new file mode 100644
index 00000000..4132d155
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- Підтримка нових типів сповіщень Mastodon 3.5
+- Кращий вигляд позначки бота і розширений вибір тем
+- Текст тепер можна вибрати у докладному поданні допису
+- Виправлено безліч помилок, включно з тою, яка перешкоджала входу на Android 6 і старіших
diff --git a/fastlane/metadata/android/vi/changelogs/91.txt b/fastlane/metadata/android/vi/changelogs/91.txt
new file mode 100644
index 00000000..2835fdfc
--- /dev/null
+++ b/fastlane/metadata/android/vi/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- Hỗ trợ những kiểu thông báo mới của Mastodon 3.5
+- Nhãn của tài khoản nhìn đẹp hơn và thay đổi theo chủ đề
+- Cho phép chọn và sao chép nội dung tút
+- Sửa lỗi chặn đăng nhập trên Android 6 trở xuống
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/83.txt b/fastlane/metadata/android/zh-Hans/changelogs/83.txt
new file mode 100644
index 00000000..e8f7c36e
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/83.txt
@@ -0,0 +1,3 @@
+Tusky v15.1
+
+此版本修复了给图片添加标题时会崩溃的问题
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/87.txt b/fastlane/metadata/android/zh-Hans/changelogs/87.txt
new file mode 100644
index 00000000..06fcd290
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky v16.0
+
+- 时间线加载逻辑完全重写,提升了流畅度、稳定性,更便于维护。
+- APNG和动画WebP格式的动态自定义表情符号。
+- 修正大量BUG
+- 支持Android 11
+- 新增界面语言支持:苏格兰盖尔语、加利西亚语、乌克兰语
+- 改进翻译