Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
6c630e08dd
|
@ -95,7 +95,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
ext.coroutinesVersion = "1.6.0"
|
||||
ext.coroutinesVersion = "1.6.1"
|
||||
ext.lifecycleVersion = "2.4.1"
|
||||
ext.roomVersion = '2.4.2'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
|
@ -112,8 +112,6 @@ repositories {
|
|||
|
||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||
|
||||
|
@ -150,6 +148,7 @@ dependencies {
|
|||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
||||
implementation "at.connyduck:kotlin-result-calladapter:1.0.1"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||
|
@ -189,8 +188,8 @@ dependencies {
|
|||
|
||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -22,6 +22,22 @@
|
|||
android:theme="@style/TuskyTheme"
|
||||
android:usesCleartextTraffic="false">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".components.login.LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
@ -30,13 +46,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
@ -84,9 +94,6 @@
|
|||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
|
|
|
@ -44,7 +44,6 @@ import androidx.appcompat.widget.PopupMenu
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -134,7 +133,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
||||
import net.accelf.yuito.FooterDrawerItem
|
||||
import net.accelf.yuito.QuickTootViewModel
|
||||
import net.accelf.yuito.streaming.StreamingManager
|
||||
|
@ -188,14 +186,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(applicationContext))
|
||||
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// delete old notification channels
|
||||
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
||||
|
||||
val activeAccount = accountManager.activeAccount
|
||||
?: return // will be redirected to LoginActivity by BaseActivity
|
||||
|
||||
|
@ -573,15 +565,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
onClick = ::logout
|
||||
}
|
||||
)
|
||||
addStickyDrawerItems(
|
||||
FooterDrawerItem().apply {
|
||||
setSubscribeProxy(
|
||||
mastodonApi.getInstance()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this@MainActivity, Lifecycle.Event.ON_DESTROY)
|
||||
)
|
||||
}
|
||||
)
|
||||
val footer = FooterDrawerItem()
|
||||
addStickyDrawerItems(footer)
|
||||
lifecycleScope.launch {
|
||||
footer.setInstance(mastodonApi.getInstance())
|
||||
}
|
||||
|
||||
if (addSearchButton) {
|
||||
binding.mainDrawer.addItemsAtPosition(
|
||||
|
@ -877,18 +865,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) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
class SplashActivity : AppCompatActivity(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(applicationContext))
|
||||
|
||||
/** delete old notification channels */
|
||||
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
||||
|
||||
/** Determine whether the user is currently logged in, and if so go ahead and load the
|
||||
* timeline. Otherwise, start the activity_login screen. */
|
||||
|
||||
val intent = if (accountManager.activeAccount != null) {
|
||||
Intent(this, MainActivity::class.java)
|
||||
} else {
|
||||
LoginActivity.getIntent(this, false)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -283,7 +283,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
return@fromCallable false
|
||||
}
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnDispose {
|
||||
|
|
|
@ -41,6 +41,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
@ -229,7 +230,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_FOLLOW: {
|
||||
if (payloadForHolder == null) {
|
||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||
holder.setMessage(concreteNotificaton.getAccount());
|
||||
holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP);
|
||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
||||
}
|
||||
break;
|
||||
|
@ -287,7 +288,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case REBLOG: {
|
||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||
}
|
||||
case FOLLOW: {
|
||||
case FOLLOW:
|
||||
case SIGN_UP: {
|
||||
return VIEW_TYPE_FOLLOW;
|
||||
}
|
||||
case FOLLOW_REQUEST: {
|
||||
|
@ -339,10 +341,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
void setMessage(TimelineAccount account) {
|
||||
void setMessage(TimelineAccount account, Boolean isSignUp) {
|
||||
Context context = message.getContext();
|
||||
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format);
|
||||
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
||||
String wholeMessage = String.format(format, wrappedDisplayName);
|
||||
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
|
||||
|
@ -599,13 +601,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
avatarRadius24dp, statusDisplayOptions.animateAvatars());
|
||||
}
|
||||
|
||||
private void setQuoteContainer(Status status, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) {
|
||||
if (status != null) {
|
||||
private void setQuoteContainer(StatusViewData.Concrete quote, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) {
|
||||
if (quote != null) {
|
||||
quoteContainer.setVisibility(View.VISIBLE);
|
||||
new QuoteInlineHelper(status, quoteContainer, listener,
|
||||
ViewQuoteInlineBinding binding = ViewQuoteInlineBinding.bind(quoteContainer);
|
||||
new QuoteInlineHelper(binding, listener,
|
||||
quoteContainer.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp),
|
||||
statusDisplayOptions)
|
||||
.setupQuoteContainer();
|
||||
.setupQuoteContainer(quote);
|
||||
} else {
|
||||
quoteContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -678,7 +681,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
||||
|
||||
setQuoteContainer(statusViewData.getStatus().getQuote(), listener, statusDisplayOptions);
|
||||
setQuoteContainer(statusViewData.getQuoteViewData(), listener, statusDisplayOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
|||
import com.google.android.material.button.MaterialButton;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Attachment.Focus;
|
||||
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
||||
|
@ -480,10 +481,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
favouriteButton.setChecked(favourited);
|
||||
}
|
||||
|
||||
private void setQuoteContainer(Status status, final StatusActionListener listener, StatusDisplayOptions statusDisplayOptions) {
|
||||
if (status != null) {
|
||||
private void setQuoteContainer(StatusViewData.Concrete quote, final StatusActionListener listener, StatusDisplayOptions statusDisplayOptions) {
|
||||
if (quote != null) {
|
||||
quoteContainer.setVisibility(View.VISIBLE);
|
||||
new QuoteInlineHelper(status, quoteContainer, listener, avatarRadius24dp, statusDisplayOptions).setupQuoteContainer();
|
||||
ViewQuoteInlineBinding binding = ViewQuoteInlineBinding.bind(quoteContainer);
|
||||
new QuoteInlineHelper(binding, listener, avatarRadius24dp, statusDisplayOptions).setupQuoteContainer(quote);
|
||||
} else {
|
||||
quoteContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -857,7 +859,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||
setReblogged(actionable.getReblogged());
|
||||
setFavourited(actionable.getFavourited());
|
||||
setQuoteContainer(actionable.getQuote(), listener, statusDisplayOptions);
|
||||
setQuoteContainer(status.getQuoteViewData(), listener, statusDisplayOptions);
|
||||
setBookmarked(actionable.getBookmarked());
|
||||
List<Attachment> attachments = actionable.getAttachments();
|
||||
boolean sensitive = actionable.getSensitive();
|
||||
|
@ -1152,9 +1154,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
StatusDisplayOptions statusDisplayOptions,
|
||||
final StatusActionListener listener
|
||||
) {
|
||||
final Card card = status.getActionable().getCard();
|
||||
final Status actionable = status.getActionable();
|
||||
final Card card = actionable.getCard();
|
||||
if (cardViewMode != CardViewMode.NONE &&
|
||||
status.getActionable().getAttachments().size() == 0 &&
|
||||
actionable.getAttachments().size() == 0 &&
|
||||
actionable.getPoll() == null &&
|
||||
card != null &&
|
||||
!TextUtils.isEmpty(card.getUrl()) &&
|
||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||
|
@ -1176,7 +1180,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||
// so let's blur the preview in that case
|
||||
// If media previews are disabled, show placeholder for cards as well
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||
|
||||
int topLeftRadius = 0;
|
||||
int topRightRadius = 0;
|
||||
|
|
|
@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify
|
|||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
|
@ -380,12 +380,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
}
|
||||
viewModel.accountFieldData.observe(
|
||||
this,
|
||||
{
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
}
|
||||
)
|
||||
this
|
||||
) {
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
}
|
||||
viewModel.noteSaved.observe(this) {
|
||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
|
@ -400,11 +399,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(
|
||||
this,
|
||||
{ isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
}
|
||||
)
|
||||
this
|
||||
) { isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
}
|
||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
|
@ -415,7 +413,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountUsernameTextView.text = usernameFormatted
|
||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||
|
||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder
|
|||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.createClickableText
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
|
||||
class AccountFieldAdapter(
|
||||
|
@ -65,7 +66,7 @@ class AccountFieldAdapter(
|
|||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||
nameTextView.text = emojifiedName
|
||||
|
||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
|
||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||
|
||||
if (field.verifiedAt != null) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
|
|||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
|
@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
rxSingle {
|
||||
mastodonApi.getInstance().getOrThrow()
|
||||
}.map { Either.Right(it) }
|
||||
}
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
|
|
|
@ -48,7 +48,8 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
|
@ -110,7 +111,7 @@ class ComposeViewModel @Inject constructor(
|
|||
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
|
||||
when (loadActually) {
|
||||
true -> Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance()
|
||||
api.getCustomEmojis(), rxSingle { api.getInstance().getOrThrow() }
|
||||
) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
|
@ -298,7 +299,7 @@ class ComposeViewModel @Inject constructor(
|
|||
): LiveData<Unit> {
|
||||
|
||||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
||||
} else {
|
||||
Observable.just(Unit)
|
||||
}.toLiveData()
|
||||
|
|
|
@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
|||
class ConversationAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener
|
||||
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(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<ConversationEntity>() {
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ConversationAccountEntity>,
|
||||
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<Emoji>,
|
||||
val favouritesCount: Int,
|
||||
|
@ -80,96 +88,44 @@ data class ConversationStatusEntity(
|
|||
val tags: List<HashTag>?,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
val collapsed: Boolean,
|
||||
val muted: Boolean,
|
||||
val poll: Poll?
|
||||
) {
|
||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ConversationStatusEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (url != other.url) return false
|
||||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
if (favourited != other.favourited) return false
|
||||
if (sensitive != other.sensitive) return false
|
||||
if (spoilerText != other.spoilerText) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (mentions != other.mentions) return false
|
||||
if (tags != other.tags) return false
|
||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
if (collapsed != other.collapsed) return false
|
||||
if (muted != other.muted) return false
|
||||
if (poll != other.poll) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + (url?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + account.hashCode()
|
||||
result = 31 * result + content.toString().hashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + emojis.hashCode()
|
||||
result = 31 * result + favouritesCount
|
||||
result = 31 * result + favourited.hashCode()
|
||||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
result = 31 * result + collapsed.hashCode()
|
||||
result = 31 * result + muted.hashCode()
|
||||
result = 31 * result + poll.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun toStatus(): Status {
|
||||
return Status(
|
||||
id = id,
|
||||
url = url,
|
||||
account = account.toAccount(),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
content = content,
|
||||
reblog = null,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = favouritesCount,
|
||||
reblogged = false,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
card = null,
|
||||
quote = null,
|
||||
fun toViewData(): StatusViewData.Concrete {
|
||||
return StatusViewData.Concrete(
|
||||
status = Status(
|
||||
id = id,
|
||||
url = url,
|
||||
account = account.toAccount(),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
content = content,
|
||||
reblog = null,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = favouritesCount,
|
||||
reblogged = false,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
card = null,
|
||||
quote = null,
|
||||
),
|
||||
isExpanded = expanded,
|
||||
isShowingContent = showingHiddenContent,
|
||||
isCollapsed = collapsed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -203,7 +159,6 @@ fun Status.toEntity() =
|
|||
tags = tags,
|
||||
showingHiddenContent = false,
|
||||
expanded = false,
|
||||
collapsible = shouldTrimStatus(content),
|
||||
collapsed = true,
|
||||
muted = muted ?: false,
|
||||
poll = poll
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/* Copyright 2022 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
data class ConversationViewData(
|
||||
val id: String,
|
||||
val accounts: List<ConversationAccountEntity>,
|
||||
val unread: Boolean,
|
||||
val lastStatus: StatusViewData.Concrete
|
||||
) {
|
||||
fun toEntity(
|
||||
accountId: Long,
|
||||
favourited: Boolean = lastStatus.status.favourited,
|
||||
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||
muted: Boolean = lastStatus.status.muted ?: false,
|
||||
poll: Poll? = lastStatus.status.poll,
|
||||
expanded: Boolean = lastStatus.isExpanded,
|
||||
collapsed: Boolean = lastStatus.isCollapsed,
|
||||
showingHiddenContent: Boolean = lastStatus.isShowingContent
|
||||
): ConversationEntity {
|
||||
return ConversationEntity(
|
||||
accountId = accountId,
|
||||
id = id,
|
||||
accounts = accounts,
|
||||
unread = unread,
|
||||
lastStatus = lastStatus.toConversationStatusEntity(
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
expanded = expanded,
|
||||
collapsed = collapsed,
|
||||
showingHiddenContent = showingHiddenContent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||
favourited: Boolean = status.favourited,
|
||||
bookmarked: Boolean = status.bookmarked,
|
||||
muted: Boolean = status.muted ?: false,
|
||||
poll: Poll? = status.poll,
|
||||
expanded: Boolean = isExpanded,
|
||||
collapsed: Boolean = isCollapsed,
|
||||
showingHiddenContent: Boolean = isShowingContent
|
||||
): ConversationStatusEntity {
|
||||
return ConversationStatusEntity(
|
||||
id = id,
|
||||
url = status.url,
|
||||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
account = status.account.toEntity(),
|
||||
content = status.content,
|
||||
createdAt = status.createdAt,
|
||||
emojis = status.emojis,
|
||||
favouritesCount = status.favouritesCount,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = status.sensitive,
|
||||
spoilerText = status.spoilerText,
|
||||
attachments = status.attachments,
|
||||
mentions = status.mentions,
|
||||
tags = status.tags,
|
||||
showingHiddenContent = showingHiddenContent,
|
||||
expanded = expanded,
|
||||
collapsed = collapsed,
|
||||
muted = muted,
|
||||
poll = poll
|
||||
)
|
||||
}
|
|
@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.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<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
|
||||
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||
statusDisplayOptions.useBlurhash());
|
||||
|
||||
if (attachments.size() == 0) {
|
||||
|
@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
mediaLabel.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent());
|
||||
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
||||
// Hide all unused views.
|
||||
mediaPreviews[0].setVisibility(View.GONE);
|
||||
mediaPreviews[1].setVisibility(View.GONE);
|
||||
|
@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
hideSensitiveMediaWarning();
|
||||
}
|
||||
|
||||
setupButtons(listener, account.getId(), status.getContent().toString(),
|
||||
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
||||
false, statusDisplayOptions);
|
||||
|
||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
|
||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
|
||||
initSwipeToRefresh()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.favourite(favourite, conversation)
|
||||
}
|
||||
}
|
||||
|
@ -165,18 +165,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.bookmark(favourite, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
|
||||
val popup = PopupMenu(requireContext(), view)
|
||||
popup.inflate(R.menu.conversation_more)
|
||||
|
||||
if (conversation.lastStatus.muted) {
|
||||
if (conversation.lastStatus.status.muted == true) {
|
||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||
} else {
|
||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||
|
@ -195,14 +195,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,13 +211,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.expandHiddenStatus(expanded, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.showContent(isShowing, conversation)
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||
}
|
||||
}
|
||||
|
@ -247,12 +247,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
reply(conversation.lastStatus.toStatus())
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
reply(conversation.lastStatus.status)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversation(conversation: ConversationEntity) {
|
||||
private fun deleteConversation(conversation: ConversationViewData) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -274,7 +274,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
adapter.item(position)?.let { conversation ->
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewModel.voteInPoll(choices, conversation)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Int>, conversation: ConversationEntity) {
|
||||
fun voteInPoll(choices: List<Int>, 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
|
@ -159,32 +158,33 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
setLoading(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val credentials: AppCredentials = try {
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
).fold(
|
||||
{ credentials ->
|
||||
// Before we open browser page we save the data.
|
||||
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||
// and we will need to pick up the process where we left off.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
// Before we open browser page we save the data.
|
||||
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||
// and we will need to pick up the process where we left off.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
},
|
||||
{ e ->
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,29 +217,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
setLoading(true)
|
||||
|
||||
val accessToken = try {
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code,
|
||||
"authorization_code"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(
|
||||
TAG,
|
||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||
)
|
||||
return
|
||||
}
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||
).fold(
|
||||
{ accessToken ->
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||
},
|
||||
{ e ->
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(
|
||||
TAG,
|
||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setLoading(loadingState: Boolean) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import android.webkit.WebStorage
|
|||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.net.toUri
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.databinding.LoginWebviewBinding
|
||||
|
@ -103,8 +104,8 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
|||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
|
||||
|
@ -115,7 +116,17 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
|||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
val url = request.url
|
||||
return shouldOverrideUrlLoading(request.url)
|
||||
}
|
||||
|
||||
/* overriding this deprecated method is necessary for it to work on api levels < 24 */
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean {
|
||||
val url = urlString?.toUri() ?: return false
|
||||
return shouldOverrideUrlLoading(url)
|
||||
}
|
||||
|
||||
fun shouldOverrideUrlLoading(url: Uri): Boolean {
|
||||
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
|
||||
val error = url.getQueryParameter("error")
|
||||
if (error != null) {
|
||||
|
@ -130,6 +141,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
webView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.notifications;
|
||||
|
||||
import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
|
||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationChannelGroup;
|
||||
import android.app.NotificationManager;
|
||||
|
@ -73,8 +76,6 @@ import java.util.concurrent.TimeUnit;
|
|||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||
|
||||
public class NotificationHelper {
|
||||
|
||||
private static int notificationId = 0;
|
||||
|
@ -116,6 +117,7 @@ public class NotificationHelper {
|
|||
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
|
||||
public static final String CHANNEL_POLL = "CHANNEL_POLL";
|
||||
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
|
||||
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
|
||||
|
||||
/**
|
||||
* WorkManager Tag
|
||||
|
@ -340,7 +342,7 @@ public class NotificationHelper {
|
|||
Status status = body.getStatus();
|
||||
|
||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||
String citedText = status.getContent().toString();
|
||||
String citedText = parseAsMastodonHtml(status.getContent()).toString();
|
||||
String inReplyToId = status.getId();
|
||||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
|
@ -392,6 +394,7 @@ public class NotificationHelper {
|
|||
CHANNEL_FAVOURITE + account.getIdentifier(),
|
||||
CHANNEL_POLL + account.getIdentifier(),
|
||||
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
|
||||
CHANNEL_SIGN_UP + account.getIdentifier(),
|
||||
};
|
||||
int[] channelNames = {
|
||||
R.string.notification_mention_name,
|
||||
|
@ -401,6 +404,7 @@ public class NotificationHelper {
|
|||
R.string.notification_favourite_name,
|
||||
R.string.notification_poll_name,
|
||||
R.string.notification_subscription_name,
|
||||
R.string.notification_sign_up_name,
|
||||
};
|
||||
int[] channelDescriptions = {
|
||||
R.string.notification_mention_descriptions,
|
||||
|
@ -410,6 +414,7 @@ public class NotificationHelper {
|
|||
R.string.notification_favourite_description,
|
||||
R.string.notification_poll_description,
|
||||
R.string.notification_subscription_description,
|
||||
R.string.notification_sign_up_description,
|
||||
};
|
||||
|
||||
List<NotificationChannel> channels = new ArrayList<>(6);
|
||||
|
@ -560,6 +565,8 @@ public class NotificationHelper {
|
|||
return account.getNotificationsFavorited();
|
||||
case POLL:
|
||||
return account.getNotificationsPolls();
|
||||
case SIGN_UP:
|
||||
return account.getNotificationsSignUps();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -582,6 +589,8 @@ public class NotificationHelper {
|
|||
return CHANNEL_FAVOURITE + account.getIdentifier();
|
||||
case POLL:
|
||||
return CHANNEL_POLL + account.getIdentifier();
|
||||
case SIGN_UP:
|
||||
return CHANNEL_SIGN_UP + account.getIdentifier();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -663,6 +672,8 @@ public class NotificationHelper {
|
|||
} else {
|
||||
return context.getString(R.string.poll_ended_voted);
|
||||
}
|
||||
case SIGN_UP:
|
||||
return String.format(context.getString(R.string.notification_sign_up_format), accountName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -671,6 +682,7 @@ public class NotificationHelper {
|
|||
switch (notification.getType()) {
|
||||
case FOLLOW:
|
||||
case FOLLOW_REQUEST:
|
||||
case SIGN_UP:
|
||||
return "@" + notification.getAccount().getUsername();
|
||||
case MENTION:
|
||||
case FAVOURITE:
|
||||
|
@ -679,13 +691,13 @@ public class NotificationHelper {
|
|||
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
||||
return notification.getStatus().getSpoilerText();
|
||||
} else {
|
||||
return notification.getStatus().getContent().toString();
|
||||
return parseAsMastodonHtml(notification.getStatus().getContent()).toString();
|
||||
}
|
||||
case POLL:
|
||||
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
||||
return notification.getStatus().getSpoilerText();
|
||||
} else {
|
||||
StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
|
||||
StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent()));
|
||||
builder.append('\n');
|
||||
Poll poll = notification.getStatus().getPoll();
|
||||
List<PollOption> options = poll.getOptions();
|
||||
|
|
|
@ -13,8 +13,8 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
||||
|
@ -216,7 +216,7 @@ class EmojiPreference(
|
|||
.setPositiveButton(R.string.restart) { _, _ ->
|
||||
// Restart the app
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
val launchIntent = Intent(context, MainActivity::class.java)
|
||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
||||
val mPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
|
|
|
@ -122,6 +122,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_sign_ups)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS
|
||||
isIconSpaceReserved = false
|
||||
isChecked = activeAccount.notificationsSignUps
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAccount { it.notificationsSignUps = newValue as Boolean }
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
|
||||
|
|
|
@ -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<String>()
|
||||
|
@ -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))
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions
|
|||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import com.keylesspalace.tusky.viewdata.toViewData
|
||||
import java.util.Date
|
||||
|
||||
|
@ -45,20 +46,21 @@ class StatusViewHolder(
|
|||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val viewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler,
|
||||
private val getStatusForPosition: (Int) -> Status?
|
||||
private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||
private val statusViewHelper = StatusViewHelper(itemView)
|
||||
|
||||
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
||||
override fun onViewMedia(v: View?, idx: Int) {
|
||||
status()?.let { status ->
|
||||
adapterHandler.showMedia(v, status, idx)
|
||||
viewdata()?.let { viewdata ->
|
||||
adapterHandler.showMedia(v, viewdata.status, idx)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean) {
|
||||
status()?.id?.let { id ->
|
||||
viewdata()?.id?.let { id ->
|
||||
viewState.setMediaShow(id, isShowing)
|
||||
}
|
||||
}
|
||||
|
@ -66,57 +68,57 @@ class StatusViewHolder(
|
|||
|
||||
init {
|
||||
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
||||
status()?.let { status ->
|
||||
adapterHandler.setStatusChecked(status, isChecked)
|
||||
viewdata()?.let { viewdata ->
|
||||
adapterHandler.setStatusChecked(viewdata.status, isChecked)
|
||||
}
|
||||
}
|
||||
binding.statusMediaPreviewContainer.clipToOutline = true
|
||||
}
|
||||
|
||||
fun bind(status: Status) {
|
||||
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
|
||||
fun bind(viewData: StatusViewData.Concrete) {
|
||||
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id)
|
||||
|
||||
updateTextView()
|
||||
|
||||
val sensitive = status.sensitive
|
||||
val sensitive = viewData.status.sensitive
|
||||
|
||||
statusViewHelper.setMediasPreview(
|
||||
statusDisplayOptions, status.attachments,
|
||||
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
|
||||
statusDisplayOptions, viewData.status.attachments,
|
||||
sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive),
|
||||
mediaViewHeight
|
||||
)
|
||||
|
||||
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
|
||||
setCreatedAt(status.createdAt)
|
||||
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
|
||||
setCreatedAt(viewData.status.createdAt)
|
||||
}
|
||||
|
||||
private fun updateTextView() {
|
||||
status()?.let { status ->
|
||||
viewdata()?.let { viewdata ->
|
||||
setupCollapsedState(
|
||||
shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
|
||||
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
|
||||
shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true),
|
||||
viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText
|
||||
)
|
||||
|
||||
if (status.spoilerText.isBlank()) {
|
||||
setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
if (viewdata.spoilerText.isBlank()) {
|
||||
setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
|
||||
binding.statusContentWarningButton.hide()
|
||||
binding.statusContentWarningDescription.hide()
|
||||
} else {
|
||||
val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
||||
val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
||||
binding.statusContentWarningDescription.text = emojiSpoiler
|
||||
binding.statusContentWarningDescription.show()
|
||||
binding.statusContentWarningButton.show()
|
||||
setContentWarningButtonText(viewState.isContentShow(status.id, true))
|
||||
setContentWarningButtonText(viewState.isContentShow(viewdata.id, true))
|
||||
binding.statusContentWarningButton.setOnClickListener {
|
||||
status()?.let { status ->
|
||||
val contentShown = viewState.isContentShow(status.id, true)
|
||||
viewdata()?.let { viewdata ->
|
||||
val contentShown = viewState.isContentShow(viewdata.id, true)
|
||||
binding.statusContentWarningDescription.invalidate()
|
||||
viewState.setContentShow(status.id, !contentShown)
|
||||
setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
viewState.setContentShow(viewdata.id, !contentShown)
|
||||
setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
|
||||
setContentWarningButtonText(!contentShown)
|
||||
}
|
||||
}
|
||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler, viewdata.status.quote != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,8 +172,8 @@ class StatusViewHolder(
|
|||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
binding.buttonToggleContent.setOnClickListener {
|
||||
status()?.let { status ->
|
||||
viewState.setCollapsed(status.id, !collapsed)
|
||||
viewdata()?.let { viewdata ->
|
||||
viewState.setCollapsed(viewdata.id, !collapsed)
|
||||
updateTextView()
|
||||
}
|
||||
}
|
||||
|
@ -190,5 +192,5 @@ class StatusViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun status() = getStatusForPosition(bindingAdapterPosition)
|
||||
private fun viewdata() = getStatusForPosition(bindingAdapterPosition)
|
||||
}
|
||||
|
|
|
@ -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, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
||||
private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int ->
|
||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||
}
|
||||
|
||||
|
@ -50,11 +50,11 @@ class StatusesAdapter(
|
|||
}
|
||||
|
||||
companion object {
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
|
||||
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||
|
@ -29,8 +26,6 @@ import com.keylesspalace.tusky.entity.HashTag
|
|||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.util.Date
|
||||
|
||||
|
@ -119,7 +114,7 @@ fun Status.toEntity(
|
|||
authorServerId = actionableStatus.account.id,
|
||||
inReplyToId = actionableStatus.inReplyToId,
|
||||
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
||||
content = actionableStatus.content.toHtml(),
|
||||
content = actionableStatus.content,
|
||||
createdAt = actionableStatus.createdAt.time,
|
||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
||||
reblogsCount = actionableStatus.reblogsCount,
|
||||
|
@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
||||
?: SpannedString(""),
|
||||
content = status.content.orEmpty(),
|
||||
createdAt = Date(status.createdAt),
|
||||
emojis = emojis,
|
||||
reblogsCount = status.reblogsCount,
|
||||
|
@ -196,7 +190,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
reblog = reblog,
|
||||
content = SpannedString(""),
|
||||
content = "",
|
||||
createdAt = Date(status.createdAt), // lie but whatever?
|
||||
emojis = listOf(),
|
||||
reblogsCount = 0,
|
||||
|
@ -225,8 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
inReplyToId = status.inReplyToId,
|
||||
inReplyToAccountId = status.inReplyToAccountId,
|
||||
reblog = null,
|
||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
||||
?: SpannedString(""),
|
||||
content = status.content.orEmpty(),
|
||||
createdAt = Date(status.createdAt),
|
||||
emojis = emojis,
|
||||
reblogsCount = status.reblogsCount,
|
||||
|
@ -252,7 +245,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
status = status,
|
||||
isExpanded = this.status.expanded,
|
||||
isShowingContent = this.status.contentShowing,
|
||||
isCollapsible = shouldTrimStatus(status.content),
|
||||
isCollapsed = this.status.contentCollapsed
|
||||
)
|
||||
}
|
||||
|
|
|
@ -43,7 +43,10 @@ import com.keylesspalace.tusky.network.FilterModel
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
@ -82,15 +85,13 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
}
|
||||
).flow
|
||||
.map { pagingData ->
|
||||
pagingData.map { timelineStatus ->
|
||||
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
||||
timelineStatus.toViewData(gson)
|
||||
}
|
||||
}
|
||||
.map { pagingData ->
|
||||
pagingData.filter { statusViewData ->
|
||||
}.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||
!shouldFilterStatus(statusViewData)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
init {
|
||||
|
|
|
@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan
|
|||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
@ -81,10 +84,11 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
|
||||
).flow
|
||||
.map { pagingData ->
|
||||
pagingData.filter { statusViewData ->
|
||||
pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||
!shouldFilterStatus(statusViewData)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
||||
|
|
|
@ -50,6 +50,7 @@ data class AccountEntity(
|
|||
var notificationsFavorited: Boolean = true,
|
||||
var notificationsPolls: Boolean = true,
|
||||
var notificationsSubscriptions: Boolean = true,
|
||||
var notificationsSignUps: Boolean = true,
|
||||
var notificationSound: Boolean = true,
|
||||
var notificationVibration: Boolean = true,
|
||||
var notificationLight: Boolean = true,
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
|||
*/
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 31)
|
||||
}, version = 33)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
@ -483,4 +483,48 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_31_32 = new Migration(31, 32) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_32_33 = new Migration(32, 33) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
|
||||
// ConversationEntity lost the s_collapsible column
|
||||
// since SQLite does not support removing columns and it is just a cache table, we recreate the whole table.
|
||||
database.execSQL("DROP TABLE `ConversationEntity`");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" +
|
||||
"`accountId` INTEGER NOT NULL," +
|
||||
"`id` TEXT NOT NULL," +
|
||||
"`accounts` TEXT NOT NULL," +
|
||||
"`unread` INTEGER NOT NULL," +
|
||||
"`s_id` TEXT NOT NULL," +
|
||||
"`s_url` TEXT," +
|
||||
"`s_inReplyToId` TEXT," +
|
||||
"`s_inReplyToAccountId` TEXT," +
|
||||
"`s_account` TEXT NOT NULL," +
|
||||
"`s_content` TEXT NOT NULL," +
|
||||
"`s_createdAt` INTEGER NOT NULL," +
|
||||
"`s_emojis` TEXT NOT NULL," +
|
||||
"`s_favouritesCount` INTEGER NOT NULL," +
|
||||
"`s_favourited` INTEGER NOT NULL," +
|
||||
"`s_bookmarked` INTEGER NOT NULL," +
|
||||
"`s_sensitive` INTEGER NOT NULL," +
|
||||
"`s_spoilerText` TEXT NOT NULL," +
|
||||
"`s_attachments` TEXT NOT NULL," +
|
||||
"`s_mentions` TEXT NOT NULL," +
|
||||
"`s_tags` TEXT," +
|
||||
"`s_showingHiddenContent` INTEGER NOT NULL," +
|
||||
"`s_expanded` INTEGER NOT NULL," +
|
||||
"`s_collapsed` INTEGER NOT NULL," +
|
||||
"`s_muted` INTEGER NOT NULL," +
|
||||
"`s_poll` TEXT," +
|
||||
"PRIMARY KEY(`id`, `accountId`))");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<Int, ConversationEntity>
|
||||
|
|
|
@ -15,9 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.db
|
||||
|
||||
import android.text.Spanned
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
|
@ -32,10 +29,8 @@ import com.keylesspalace.tusky.entity.HashTag
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -144,22 +139,6 @@ class Converters @Inject constructor (
|
|||
return Date(date)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun spannedToString(spanned: Spanned?): String? {
|
||||
if (spanned == null) {
|
||||
return null
|
||||
}
|
||||
return spanned.toHtml()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToSpanned(spannedString: String?): Spanned? {
|
||||
if (spannedString == null) {
|
||||
return null
|
||||
}
|
||||
return spannedString.parseAsHtml().trimTrailingWhitespace()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun pollToJson(poll: Poll?): String? {
|
||||
return gson.toJson(poll)
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.keylesspalace.tusky.FiltersActivity
|
|||
import com.keylesspalace.tusky.LicenseActivity
|
||||
import com.keylesspalace.tusky.ListsActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
|
@ -118,6 +119,9 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun contributesDraftActivity(): DraftsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSplashActivity(): SplashActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesAccessTokenLoginActivity(): AccessTokenLoginActivity
|
||||
}
|
||||
|
|
|
@ -68,7 +68,8 @@ class AppModule {
|
|||
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
||||
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31
|
||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||
AppDatabase.MIGRATION_32_33
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -18,12 +18,10 @@ package com.keylesspalace.tusky.di
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.text.Spanned
|
||||
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
|
@ -53,11 +51,7 @@ class NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGson(): Gson {
|
||||
return GsonBuilder()
|
||||
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
|
||||
.create()
|
||||
}
|
||||
fun providesGson() = Gson()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -114,6 +108,7 @@ class NetworkModule {
|
|||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.text.Spanned
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.Date
|
||||
|
||||
|
@ -24,7 +23,7 @@ data class Account(
|
|||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct", alternate = ["subject"]) val username: String,
|
||||
@SerializedName("display_name") private val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||
val note: Spanned,
|
||||
val note: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val header: String,
|
||||
|
@ -54,56 +53,6 @@ data class Account(
|
|||
get() = displayName.orEmpty()
|
||||
|
||||
fun isRemote(): Boolean = this.username != this.localUsername
|
||||
|
||||
/**
|
||||
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as Account
|
||||
|
||||
if (id != other.id) return false
|
||||
if (localUsername != other.localUsername) return false
|
||||
if (username != other.username) return false
|
||||
if (displayName != other.displayName) return false
|
||||
if (note.toString() != other.note.toString()) return false
|
||||
if (url != other.url) return false
|
||||
if (avatar != other.avatar) return false
|
||||
if (header != other.header) return false
|
||||
if (locked != other.locked) return false
|
||||
if (followersCount != other.followersCount) return false
|
||||
if (followingCount != other.followingCount) return false
|
||||
if (statusesCount != other.statusesCount) return false
|
||||
if (source != other.source) return false
|
||||
if (bot != other.bot) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (fields != other.fields) return false
|
||||
if (moved != other.moved) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + localUsername.hashCode()
|
||||
result = 31 * result + username.hashCode()
|
||||
result = 31 * result + (displayName?.hashCode() ?: 0)
|
||||
result = 31 * result + note.toString().hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + avatar.hashCode()
|
||||
result = 31 * result + header.hashCode()
|
||||
result = 31 * result + locked.hashCode()
|
||||
result = 31 * result + followersCount
|
||||
result = 31 * result + followingCount
|
||||
result = 31 * result + statusesCount
|
||||
result = 31 * result + (source?.hashCode() ?: 0)
|
||||
result = 31 * result + bot.hashCode()
|
||||
result = 31 * result + (emojis?.hashCode() ?: 0)
|
||||
result = 31 * result + (fields?.hashCode() ?: 0)
|
||||
result = 31 * result + (moved?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class AccountSource(
|
||||
|
@ -115,7 +64,7 @@ data class AccountSource(
|
|||
|
||||
data class Field(
|
||||
val name: String,
|
||||
val value: Spanned,
|
||||
val value: String,
|
||||
@SerializedName("verified_at") val verifiedAt: Date?
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -37,7 +37,9 @@ data class Notification(
|
|||
FOLLOW("follow"),
|
||||
FOLLOW_REQUEST("follow_request"),
|
||||
POLL("poll"),
|
||||
STATUS("status");
|
||||
STATUS("status"),
|
||||
SIGN_UP("admin.sign_up"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -49,7 +51,7 @@ data class Notification(
|
|||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS)
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
|
@ -16,10 +16,9 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.ArrayList
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import java.util.Date
|
||||
|
||||
data class Status(
|
||||
|
@ -29,7 +28,7 @@ data class Status(
|
|||
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||
val reblog: Status?,
|
||||
val content: Spanned,
|
||||
val content: String,
|
||||
@SerializedName("created_at", alternate = ["published"]) val createdAt: Date,
|
||||
val emojis: List<Emoji>,
|
||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||
|
@ -143,8 +142,9 @@ data class Status(
|
|||
}
|
||||
|
||||
private fun getEditableText(): String {
|
||||
val builder = SpannableStringBuilder(content)
|
||||
for (span in content.getSpans(0, content.length, URLSpan::class.java)) {
|
||||
val contentSpanned = content.parseAsMastodonHtml()
|
||||
val builder = SpannableStringBuilder(content.parseAsMastodonHtml())
|
||||
for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) {
|
||||
val url = span.url
|
||||
for ((_, url1, username) in mentions) {
|
||||
if (url == url1) {
|
||||
|
@ -158,71 +158,6 @@ data class Status(
|
|||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as Status
|
||||
|
||||
if (id != other.id) return false
|
||||
if (url != other.url) return false
|
||||
if (account != other.account) return false
|
||||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (reblog != other.reblog) return false
|
||||
if (content.toString() != other.content.toString()) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (reblogsCount != other.reblogsCount) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
if (reblogged != other.reblogged) return false
|
||||
if (favourited != other.favourited) return false
|
||||
if (bookmarked != other.bookmarked) return false
|
||||
if (sensitive != other.sensitive) return false
|
||||
if (spoilerText != other.spoilerText) return false
|
||||
if (visibility != other.visibility) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (mentions != other.mentions) return false
|
||||
if (tags != other.tags) return false
|
||||
if (application != other.application) return false
|
||||
if (pinned != other.pinned) return false
|
||||
if (muted != other.muted) return false
|
||||
if (poll != other.poll) return false
|
||||
if (card != other.card) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + (url?.hashCode() ?: 0)
|
||||
result = 31 * result + account.hashCode()
|
||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + (reblog?.hashCode() ?: 0)
|
||||
result = 31 * result + content.toString().hashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + emojis.hashCode()
|
||||
result = 31 * result + reblogsCount
|
||||
result = 31 * result + favouritesCount
|
||||
result = 31 * result + reblogged.hashCode()
|
||||
result = 31 * result + favourited.hashCode()
|
||||
result = 31 * result + bookmarked.hashCode()
|
||||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + visibility.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + (tags?.hashCode() ?: 0)
|
||||
result = 31 * result + (application?.hashCode() ?: 0)
|
||||
result = 31 * result + (pinned?.hashCode() ?: 0)
|
||||
result = 31 * result + (muted?.hashCode() ?: 0)
|
||||
result = 31 * result + (poll?.hashCode() ?: 0)
|
||||
result = 31 * result + (card?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val url: String,
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.fragment;
|
||||
|
||||
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
|
||||
import static autodispose2.AutoDispose.autoDisposable;
|
||||
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -114,10 +118,6 @@ import kotlin.Unit;
|
|||
import kotlin.collections.CollectionsKt;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
import static autodispose2.AutoDispose.autoDisposable;
|
||||
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
|
||||
|
||||
public class NotificationsFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener,
|
||||
StatusActionListener,
|
||||
|
@ -716,6 +716,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
return getString(R.string.notification_poll_name);
|
||||
case STATUS:
|
||||
return getString(R.string.notification_subscription_name);
|
||||
case SIGN_UP:
|
||||
return getString(R.string.notification_sign_up_name);
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.json
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
import org.jsoup.Jsoup
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class SpannedTypeAdapter : JsonDeserializer<Spanned>, JsonSerializer<Spanned?> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned {
|
||||
return json.asString
|
||||
/* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api.
|
||||
* We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior.
|
||||
*/
|
||||
?.replace("<br> ", "<br> ")
|
||||
?.replace("<br /> ", "<br /> ")
|
||||
?.replace("<br/> ", "<br/> ")
|
||||
?.replace(" ", " ")
|
||||
?.let { html ->
|
||||
Jsoup.parse(html)
|
||||
.apply {
|
||||
select(".quote-inline").forEach { it.remove() }
|
||||
}
|
||||
.html()
|
||||
}
|
||||
?.parseAsHtml()
|
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||
* most status contents do, so it should be trimmed. */
|
||||
?.trimTrailingWhitespace()
|
||||
?: SpannedString("")
|
||||
}
|
||||
|
||||
override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL))
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ import retrofit2.http.Query
|
|||
interface MastodonApi {
|
||||
|
||||
companion object {
|
||||
const val ENDPOINT_AUTHORIZE = "/oauth/authorize"
|
||||
const val ENDPOINT_AUTHORIZE = "oauth/authorize"
|
||||
const val DOMAIN_HEADER = "domain"
|
||||
const val PLACEHOLDER_DOMAIN = "dummy.placeholder"
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ interface MastodonApi {
|
|||
fun getCustomEmojis(): Single<List<Emoji>>
|
||||
|
||||
@GET("api/v1/instance")
|
||||
fun getInstance(): Single<Instance>
|
||||
suspend fun getInstance(): Result<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
fun getFilters(): Single<List<Filter>>
|
||||
|
@ -249,12 +249,12 @@ interface MastodonApi {
|
|||
): Single<List<ScheduledStatus>>
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
fun deleteScheduledStatus(
|
||||
suspend fun deleteScheduledStatus(
|
||||
@Path("id") scheduledStatusId: String
|
||||
): Single<ResponseBody>
|
||||
): Result<ResponseBody>
|
||||
|
||||
@GET("api/v1/accounts/verify_credentials")
|
||||
fun accountVerifyCredentials(): Single<Account>
|
||||
suspend fun accountVerifyCredentials(): Result<Account>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
|
@ -265,7 +265,7 @@ interface MastodonApi {
|
|||
|
||||
@Multipart
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
fun accountUpdateCredentials(
|
||||
suspend fun accountUpdateCredentials(
|
||||
@Part(value = "display_name") displayName: RequestBody?,
|
||||
@Part(value = "note") note: RequestBody?,
|
||||
@Part(value = "locked") locked: RequestBody?,
|
||||
|
@ -279,7 +279,7 @@ interface MastodonApi {
|
|||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||
): Call<Account>
|
||||
): Result<Account>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
|
@ -447,7 +447,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uris") redirectUris: String,
|
||||
@Field("scopes") scopes: String,
|
||||
@Field("website") website: String
|
||||
): AppCredentials
|
||||
): Result<AppCredentials>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
|
@ -458,7 +458,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uri") redirectUri: String,
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grantType: String
|
||||
): AccessToken
|
||||
): Result<AccessToken>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
|
|
|
@ -69,6 +69,7 @@ object PrefKeys {
|
|||
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
|
||||
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
|
||||
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
|
||||
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
|
||||
|
||||
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
|
||||
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/* Copyright 2022 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
@file:JvmName("StatusParsingHelper")
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.core.text.parseAsHtml
|
||||
import org.jsoup.Jsoup.parse
|
||||
|
||||
/**
|
||||
* parse a String containing html from the Mastodon api to Spanned
|
||||
*/
|
||||
fun String.parseAsMastodonHtml(): Spanned {
|
||||
return this.replace("<br> ", "<br> ")
|
||||
.replace("<br /> ", "<br /> ")
|
||||
.replace("<br/> ", "<br/> ")
|
||||
.replace(" ", " ")
|
||||
.let { parse(it) }
|
||||
.apply {
|
||||
select(".quote-inline").forEach { it.remove() }
|
||||
}
|
||||
.html()
|
||||
.parseAsHtml()
|
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||
* most status contents do, so it should be trimmed. */
|
||||
.trimTrailingWhitespace()
|
||||
}
|
||||
|
||||
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
||||
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
||||
}
|
||||
|
||||
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
||||
var replacing = false
|
||||
var builder: SpannableStringBuilder? = null
|
||||
val length = content.length
|
||||
for (index in 0 until length) {
|
||||
val character = content[index]
|
||||
|
||||
// If there are more than one or two, switch to a map
|
||||
if (character == SOFT_HYPHEN) {
|
||||
if (!replacing) {
|
||||
replacing = true
|
||||
builder = SpannableStringBuilder(content, 0, index)
|
||||
}
|
||||
builder!!.append(ASCII_HYPHEN)
|
||||
} else if (replacing) {
|
||||
builder!!.append(character)
|
||||
}
|
||||
}
|
||||
return if (replacing) builder else content
|
||||
}
|
||||
|
||||
private const val SOFT_HYPHEN = '\u00ad'
|
||||
private const val ASCII_HYPHEN = '-'
|
|
@ -27,12 +27,9 @@ fun Status.toViewData(
|
|||
isExpanded: Boolean,
|
||||
isCollapsed: Boolean
|
||||
): StatusViewData.Concrete {
|
||||
val visibleStatus = this.reblog ?: this
|
||||
|
||||
return StatusViewData.Concrete(
|
||||
status = this,
|
||||
isShowingContent = isShowingContent,
|
||||
isCollapsible = shouldTrimStatus(visibleStatus.content),
|
||||
isCollapsed = isCollapsed,
|
||||
isExpanded = isExpanded,
|
||||
)
|
||||
|
|
|
@ -15,9 +15,11 @@
|
|||
package com.keylesspalace.tusky.viewdata
|
||||
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.replaceCrashingCharacters
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
|
||||
/**
|
||||
* Created by charlag on 11/07/2017.
|
||||
|
@ -32,13 +34,6 @@ sealed class StatusViewData {
|
|||
val status: Status,
|
||||
val isExpanded: Boolean,
|
||||
val isShowingContent: Boolean,
|
||||
/**
|
||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||
* all content regardless.
|
||||
*
|
||||
* @return Whether the post is collapsible or never collapsed.
|
||||
*/
|
||||
val isCollapsible: Boolean,
|
||||
/**
|
||||
* Specifies whether the content of this post is currently limited in visibility to the first
|
||||
* 500 characters or not.
|
||||
|
@ -51,6 +46,14 @@ sealed class StatusViewData {
|
|||
override val id: String
|
||||
get() = status.id
|
||||
|
||||
/**
|
||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||
* all content regardless.
|
||||
*
|
||||
* @return Whether the post is collapsible or never collapsed.
|
||||
*/
|
||||
val isCollapsible: Boolean
|
||||
|
||||
val content: Spanned
|
||||
val spoilerText: String
|
||||
val username: String
|
||||
|
@ -71,48 +74,23 @@ sealed class StatusViewData {
|
|||
val rebloggingStatus: Status?
|
||||
get() = if (status.reblog != null) status else null
|
||||
|
||||
val quoteViewData =
|
||||
status.quote?.let { Concrete(it, isExpanded, isShowingContent, isCollapsed) }
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT == 23) {
|
||||
// https://github.com/tuskyapp/Tusky/issues/563
|
||||
this.content = replaceCrashingCharacters(status.actionableStatus.content)
|
||||
this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml())
|
||||
this.spoilerText =
|
||||
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
||||
this.username =
|
||||
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
||||
} else {
|
||||
this.content = status.actionableStatus.content
|
||||
this.content = status.actionableStatus.content.parseAsMastodonHtml()
|
||||
this.spoilerText = status.actionableStatus.spoilerText
|
||||
this.username = status.actionableStatus.account.username
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SOFT_HYPHEN = '\u00ad'
|
||||
private const val ASCII_HYPHEN = '-'
|
||||
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
||||
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
||||
}
|
||||
|
||||
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
||||
var replacing = false
|
||||
var builder: SpannableStringBuilder? = null
|
||||
val length = content.length
|
||||
for (index in 0 until length) {
|
||||
val character = content[index]
|
||||
|
||||
// If there are more than one or two, switch to a map
|
||||
if (character == SOFT_HYPHEN) {
|
||||
if (!replacing) {
|
||||
replacing = true
|
||||
builder = SpannableStringBuilder(content, 0, index)
|
||||
}
|
||||
builder!!.append(ASCII_HYPHEN)
|
||||
} else if (replacing) {
|
||||
builder!!.append(character)
|
||||
}
|
||||
}
|
||||
return if (replacing) builder else content
|
||||
}
|
||||
this.isCollapsible = shouldTrimStatus(this.content)
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
|
|
|
@ -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<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
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<Account>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import autodispose2.SingleSubscribeProxy
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemDrawerFooterBinding
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
|
@ -35,14 +34,13 @@ class FooterDrawerItem : AbstractDrawerItem<FooterDrawerItem, BindingHolder<Item
|
|||
|
||||
override fun getViewHolder(v: View): BindingHolder<ItemDrawerFooterBinding> = throw UnsupportedOperationException()
|
||||
|
||||
fun setSubscribeProxy(subscribeProxy: SingleSubscribeProxy<Instance>) {
|
||||
subscribeProxy.subscribe(
|
||||
{ instance ->
|
||||
binding.instanceData.text = String.format("%s\n%s\n%s", instance.title, instance.uri, instance.version)
|
||||
},
|
||||
{
|
||||
binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
|
||||
}
|
||||
)
|
||||
fun setInstance(instance: Result<Instance>) {
|
||||
instance
|
||||
.onSuccess {
|
||||
binding.instanceData.text = String.format("%s\n%s\n%s", it.title, it.uri, it.version)
|
||||
}
|
||||
.onFailure {
|
||||
binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
package net.accelf.yuito;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Px;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteInlineHelper {
|
||||
private final Status quoteStatus;
|
||||
|
||||
private final View quoteContainer;
|
||||
private final ImageView quoteAvatar;
|
||||
private final TextView quoteDisplayName;
|
||||
private final TextView quoteUsername;
|
||||
private final TextView quoteContentWarningDescription;
|
||||
private final MaterialButton quoteContentWarningButton;
|
||||
private final TextView quoteContent;
|
||||
private final TextView quoteMedia;
|
||||
|
||||
private final LinkListener listener;
|
||||
@Px
|
||||
private final int avatarRadius24dp;
|
||||
private final StatusDisplayOptions statusDisplayOptions;
|
||||
|
||||
public QuoteInlineHelper(Status status, View container, LinkListener listener,
|
||||
@Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
|
||||
quoteStatus = status;
|
||||
quoteContainer = container;
|
||||
quoteAvatar = container.findViewById(R.id.status_quote_inline_avatar);
|
||||
quoteDisplayName = container.findViewById(R.id.status_quote_inline_display_name);
|
||||
quoteUsername = container.findViewById(R.id.status_quote_inline_username);
|
||||
quoteContentWarningDescription = container.findViewById(R.id.status_quote_inline_content_warning_description);
|
||||
quoteContentWarningButton = container.findViewById(R.id.status_quote_inline_content_warning_button);
|
||||
quoteContent = container.findViewById(R.id.status_quote_inline_content);
|
||||
quoteMedia = container.findViewById(R.id.status_quote_inline_media);
|
||||
this.listener = listener;
|
||||
this.avatarRadius24dp = avatarRadius24dp;
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
private void setDisplayName(String name, List<Emoji> customEmojis) {
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, quoteDisplayName, statusDisplayOptions.animateEmojis());
|
||||
quoteDisplayName.setText(emojifiedName);
|
||||
}
|
||||
|
||||
private void setUsername(String name) {
|
||||
Context context = quoteUsername.getContext();
|
||||
String format = context.getString(R.string.post_username_format);
|
||||
String usernameText = String.format(format, name);
|
||||
quoteUsername.setText(usernameText);
|
||||
}
|
||||
|
||||
private void setContent(
|
||||
Spanned content,
|
||||
List<Status.Mention> mentions,
|
||||
List<HashTag> tags,
|
||||
List<Emoji> emojis,
|
||||
LinkListener listener
|
||||
) {
|
||||
Spanned singleLineText = SpannedTextHelper.replaceSpanned(content);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(singleLineText, emojis, quoteContent, statusDisplayOptions.animateEmojis());
|
||||
LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, tags, listener);
|
||||
}
|
||||
|
||||
private void setAvatar(String url, @Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
|
||||
ImageLoadingHelper.loadAvatar(url, quoteAvatar, avatarRadius24dp, statusDisplayOptions.animateAvatars());
|
||||
}
|
||||
|
||||
private void setSpoilerText(String spoilerText, List<Emoji> emojis) {
|
||||
CharSequence emojiSpoiler =
|
||||
CustomEmojiHelper.emojify(spoilerText, emojis, quoteContentWarningDescription, statusDisplayOptions.animateEmojis());
|
||||
quoteContentWarningDescription.setText(emojiSpoiler);
|
||||
quoteContentWarningDescription.setVisibility(View.VISIBLE);
|
||||
quoteContentWarningButton.setVisibility(View.VISIBLE);
|
||||
quoteContentWarningButton.setOnClickListener(v
|
||||
-> setContentVisibility(!(quoteContent.getVisibility() == View.VISIBLE)));
|
||||
setContentVisibility(false);
|
||||
}
|
||||
|
||||
private void setContentVisibility(boolean show) {
|
||||
if (show) {
|
||||
quoteContent.setVisibility(View.VISIBLE);
|
||||
quoteContentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||
} else {
|
||||
quoteContent.setVisibility(View.GONE);
|
||||
quoteContentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSpoilerText() {
|
||||
quoteContentWarningDescription.setVisibility(View.GONE);
|
||||
quoteContentWarningButton.setVisibility(View.GONE);
|
||||
quoteContent.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setOnClickListener(String accountId, String statusUrl) {
|
||||
quoteAvatar.setOnClickListener(view -> listener.onViewAccount(accountId));
|
||||
quoteDisplayName.setOnClickListener(view -> listener.onViewAccount(accountId));
|
||||
quoteUsername.setOnClickListener(view -> listener.onViewAccount(accountId));
|
||||
quoteContent.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
|
||||
quoteMedia.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
|
||||
quoteContainer.setOnClickListener(view -> listener.onViewUrl(statusUrl, statusUrl));
|
||||
}
|
||||
|
||||
public void setupQuoteContainer() {
|
||||
TimelineAccount account = quoteStatus.getAccount();
|
||||
setDisplayName(account.getName(), account.getEmojis());
|
||||
setUsername(account.getUsername());
|
||||
setContent(
|
||||
quoteStatus.getContent(),
|
||||
quoteStatus.getMentions(),
|
||||
quoteStatus.getTags(),
|
||||
quoteStatus.getEmojis(),
|
||||
listener
|
||||
);
|
||||
setAvatar(account.getAvatar(), avatarRadius24dp, statusDisplayOptions);
|
||||
setOnClickListener(account.getId(), quoteStatus.getUrl());
|
||||
|
||||
if (quoteStatus.getSpoilerText().isEmpty()) {
|
||||
hideSpoilerText();
|
||||
} else {
|
||||
setSpoilerText(quoteStatus.getSpoilerText(), quoteStatus.getEmojis());
|
||||
}
|
||||
|
||||
if (quoteStatus.getAttachments().size() == 0) {
|
||||
quoteMedia.setVisibility(View.GONE);
|
||||
} else {
|
||||
quoteMedia.setVisibility(View.VISIBLE);
|
||||
quoteMedia.setText(quoteContainer.getContext().getString(R.string.status_quote_media,
|
||||
quoteStatus.getAttachments().size()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package net.accelf.yuito
|
||||
|
||||
import android.text.Spanned
|
||||
import android.view.View
|
||||
import androidx.annotation.Px
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status.Mention
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
class QuoteInlineHelper(
|
||||
private val binding: ViewQuoteInlineBinding,
|
||||
private val listener: LinkListener,
|
||||
@Px private val avatarRadius24dp: Int,
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
|
||||
private fun setDisplayName(name: String, customEmojis: List<Emoji>?) {
|
||||
val viewDisplayName = binding.statusQuoteInlineDisplayName
|
||||
val emojifiedName = name.emojify(customEmojis, viewDisplayName, statusDisplayOptions.animateEmojis)
|
||||
viewDisplayName.text = emojifiedName
|
||||
}
|
||||
|
||||
private fun setUsername(name: String) {
|
||||
val viewUserName = binding.statusQuoteInlineUsername
|
||||
val context = viewUserName.context
|
||||
val format = context.getString(R.string.post_username_format)
|
||||
val usernameText = String.format(format, name)
|
||||
viewUserName.text = usernameText
|
||||
}
|
||||
|
||||
private fun setContent(
|
||||
content: Spanned,
|
||||
mentions: List<Mention>,
|
||||
tags: List<HashTag>?,
|
||||
emojis: List<Emoji>,
|
||||
) {
|
||||
val viewContent = binding.statusQuoteInlineContent
|
||||
val singleLineText = SpannedTextHelper.replaceSpanned(content)
|
||||
val emojifiedText = singleLineText.emojify(emojis, viewContent, statusDisplayOptions.animateEmojis)
|
||||
setClickableText(viewContent, emojifiedText, mentions, tags, listener)
|
||||
}
|
||||
|
||||
private fun setAvatar(url: String, @Px avatarRadius24dp: Int, statusDisplayOptions: StatusDisplayOptions) {
|
||||
loadAvatar(url, binding.statusQuoteInlineAvatar, avatarRadius24dp, statusDisplayOptions.animateAvatars)
|
||||
}
|
||||
|
||||
private fun setSpoilerText(spoilerText: String, emojis: List<Emoji>) {
|
||||
val viewDescription = binding.statusQuoteInlineContentWarningDescription
|
||||
val viewButton = binding.statusQuoteInlineContentWarningButton
|
||||
val emojiSpoiler = spoilerText.emojify(emojis, viewDescription, statusDisplayOptions.animateEmojis)
|
||||
viewDescription.text = emojiSpoiler
|
||||
viewDescription.visibility = View.VISIBLE
|
||||
viewButton.visibility = View.VISIBLE
|
||||
viewButton.setOnClickListener {
|
||||
setContentVisibility(binding.statusQuoteInlineContent.visibility != View.VISIBLE)
|
||||
}
|
||||
setContentVisibility(false)
|
||||
}
|
||||
|
||||
private fun setContentVisibility(show: Boolean) {
|
||||
binding.statusQuoteInlineContent.visibility = when (show) {
|
||||
true -> View.VISIBLE
|
||||
false -> View.GONE
|
||||
}
|
||||
binding.statusQuoteInlineContentWarningButton.setText(when (show) {
|
||||
true -> R.string.post_content_warning_show_less
|
||||
false -> R.string.post_content_warning_show_more
|
||||
})
|
||||
}
|
||||
|
||||
private fun hideSpoilerText() {
|
||||
binding.statusQuoteInlineContentWarningDescription.visibility = View.GONE
|
||||
binding.statusQuoteInlineContentWarningButton.visibility = View.GONE
|
||||
binding.statusQuoteInlineContent.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun setOnClickListener(accountId: String, statusUrl: String?) {
|
||||
binding.statusQuoteInlineAvatar.setOnClickListener { listener.onViewAccount(accountId) }
|
||||
binding.statusQuoteInlineDisplayName.setOnClickListener { listener.onViewAccount(accountId) }
|
||||
binding.statusQuoteInlineUsername.setOnClickListener { listener.onViewAccount(accountId) }
|
||||
binding.statusQuoteInlineContent.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
|
||||
binding.statusQuoteInlineMedia.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
|
||||
binding.root.setOnClickListener { listener.onViewUrl(statusUrl!!, statusUrl) }
|
||||
}
|
||||
|
||||
fun setupQuoteContainer(quote: StatusViewData.Concrete) {
|
||||
val actionable = quote.actionable
|
||||
val account = actionable.account
|
||||
setDisplayName(account.name, account.emojis)
|
||||
setUsername(account.username)
|
||||
setContent(
|
||||
quote.content,
|
||||
actionable.mentions,
|
||||
actionable.tags,
|
||||
actionable.emojis,
|
||||
)
|
||||
setAvatar(account.avatar, avatarRadius24dp, statusDisplayOptions)
|
||||
setOnClickListener(account.id, actionable.url)
|
||||
if (quote.spoilerText.isEmpty()) {
|
||||
hideSpoilerText()
|
||||
} else {
|
||||
setSpoilerText(quote.spoilerText, actionable.emojis)
|
||||
}
|
||||
val viewMedia = binding.statusQuoteInlineMedia
|
||||
if (actionable.attachments.size == 0) {
|
||||
viewMedia.visibility = View.GONE
|
||||
} else {
|
||||
viewMedia.visibility = View.VISIBLE
|
||||
viewMedia.text = viewMedia.context.getString(R.string.status_quote_media, actionable.attachments.size)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -253,9 +253,6 @@
|
|||
<plurals name="hint_describe_for_visually_impaired">
|
||||
<item quantity="one">Mìnich e dhan fheadhainn air a bheil cion-lèirsinn
|
||||
\n(%d caractar(an) air a char as fhaide)</item>
|
||||
<item quantity="two"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="error_failed_set_caption">Cha deach leinn am fo-thiotal a shuidheachadh</string>
|
||||
<string name="compose_active_account_description">A’ postadh leis a’ chunntas %1$s</string>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
<string name="action_follow">Seguir</string>
|
||||
<string name="action_logout_confirm">Tes a certeza de que queres desconectar a conta %1$s\?</string>
|
||||
<string name="action_logout">Desconectar</string>
|
||||
<string name="action_login">Conecta con Mastodon</string>
|
||||
<string name="action_login">Accede con Mastodon</string>
|
||||
<string name="action_compose">Redactar</string>
|
||||
<string name="action_more">Máis</string>
|
||||
<string name="action_unfavourite">Eliminar favorito</string>
|
||||
|
@ -116,7 +116,7 @@
|
|||
<string name="error_video_upload_size">Os ficheiros de vídeo teñen que ser menores de 40MB.</string>
|
||||
<string name="error_image_upload_size">O ficheiro debe ser menor de 8MB.</string>
|
||||
<string name="error_compose_character_limit">A publicación é demasiado longa!</string>
|
||||
<string name="error_retrieving_oauth_token">Fallou a obtención do token de conexión.</string>
|
||||
<string name="error_retrieving_oauth_token">Fallou a obtención do token de acceso.</string>
|
||||
<string name="error_authorization_denied">A autorización foi rexeitada.</string>
|
||||
<string name="error_authorization_unknown">Aconteceu un erro non identificado de autorización.</string>
|
||||
<string name="error_no_web_browser_found">Non se atopou un navegador para utilizar.</string>
|
||||
|
@ -411,8 +411,8 @@
|
|||
<string name="dialog_block_warning">Bloquear @%s\?</string>
|
||||
<string name="mute_domain_warning_dialog_ok">Agochar todo o dominio</string>
|
||||
<string name="mute_domain_warning">Tes a certeza de querer bloquear a todo %s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas.</string>
|
||||
<string name="dialog_redraft_post_warning">Eliminar e reescribir este toot\?</string>
|
||||
<string name="dialog_delete_post_warning">Eliminar este toot\?</string>
|
||||
<string name="dialog_redraft_post_warning">Eliminar e reescribir esta publicación\?</string>
|
||||
<string name="dialog_delete_post_warning">Eliminar esta publicación\?</string>
|
||||
<string name="dialog_unfollow_warning">Deixar de seguir esta conta\?</string>
|
||||
<string name="dialog_message_cancel_follow_request">Revogar a solicitude de seguimento\?</string>
|
||||
<string name="dialog_download_image">Descargar</string>
|
||||
|
|
|
@ -21,23 +21,23 @@
|
|||
<string name="error_authorization_unknown">Óskilgreind auðkenningarvilla kom upp.</string>
|
||||
<string name="error_authorization_denied">Heimild var hafnað.</string>
|
||||
<string name="error_retrieving_oauth_token">Mistókst að fá innskráningarteikn.</string>
|
||||
<string name="error_compose_character_limit">Stöðufærslan er of löng!</string>
|
||||
<string name="error_compose_character_limit">Færslan er of löng!</string>
|
||||
<string name="error_image_upload_size">Skráin verður að vera minni en 8MB.</string>
|
||||
<string name="error_video_upload_size">Myndskeiðaskrár verða að vera minni en 40MB.</string>
|
||||
<string name="error_media_upload_type">Þessa tegund skrár er ekki hægt að senda inn.</string>
|
||||
<string name="error_media_upload_opening">Ekki var hægt að opna skrána.</string>
|
||||
<string name="error_media_upload_permission">Krafist er heimilda til að lesa gögn.</string>
|
||||
<string name="error_media_download_permission">Krafist er heimilda til að geyma gögn.</string>
|
||||
<string name="error_media_upload_image_or_video">Ekki er hægt að hengja bæði myndir og myndskeið við sömu stöðufærslu.</string>
|
||||
<string name="error_media_upload_image_or_video">Ekki er hægt að hengja bæði myndir og myndskeið við sömu færslu.</string>
|
||||
<string name="error_media_upload_sending">Sendingin mistókst.</string>
|
||||
<string name="error_sender_account_gone">Villa við að senda tíst.</string>
|
||||
<string name="error_sender_account_gone">Villa við að senda færslu.</string>
|
||||
<string name="title_home">Heim</string>
|
||||
<string name="title_notifications">Tilkynningar</string>
|
||||
<string name="title_public_local">Staðvært</string>
|
||||
<string name="title_public_federated">Sameiginlegt</string>
|
||||
<string name="title_direct_messages">Bein skilaboð</string>
|
||||
<string name="title_tab_preferences">Flipar</string>
|
||||
<string name="title_view_thread">Tíst</string>
|
||||
<string name="title_view_thread">Þráður</string>
|
||||
<string name="title_posts">Færslur</string>
|
||||
<string name="title_posts_with_replies">Með svörum</string>
|
||||
<string name="title_posts_pinned">Fest</string>
|
||||
|
@ -62,8 +62,8 @@
|
|||
<string name="post_content_show_less">Fella saman</string>
|
||||
<string name="message_empty">Ekkert hér.</string>
|
||||
<string name="footer_empty">Ekkert hér. Togaðu niður til að endurhlaða!</string>
|
||||
<string name="notification_reblog_format">%s endurbirti tístið þitt</string>
|
||||
<string name="notification_favourite_format">%s setti tíst frá þér í eftirlæti</string>
|
||||
<string name="notification_reblog_format">%s endurbirti færsluna þína</string>
|
||||
<string name="notification_favourite_format">%s setti færslu frá þér í eftirlæti</string>
|
||||
<string name="notification_follow_format">%s fylgist núna með þér</string>
|
||||
<string name="report_username_format">Kæra @%s</string>
|
||||
<string name="report_comment_hint">Aðrar athugasemdir\?</string>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<string name="action_reject">Hafna</string>
|
||||
<string name="action_access_drafts">Drög</string>
|
||||
<string name="action_access_scheduled_posts">Áætluð tíst</string>
|
||||
<string name="action_toggle_visibility">Sýnileiki tísts</string>
|
||||
<string name="action_toggle_visibility">Sýnileiki færslu</string>
|
||||
<string name="action_content_warning">Aðvörun vegna efnis</string>
|
||||
<string name="action_emoji_keyboard">Lyklaborð með tjáningartáknum</string>
|
||||
<string name="action_schedule_post">Tímasetja tíst</string>
|
||||
|
@ -234,9 +234,9 @@
|
|||
<string name="notification_follow_name">Nýir fylgjendur</string>
|
||||
<string name="notification_follow_description">Tilkynningar um nýja fylgjendur</string>
|
||||
<string name="notification_boost_name">Endurbirtingar</string>
|
||||
<string name="notification_boost_description">Tilkynningar þegar tístin þín eru endurbirt</string>
|
||||
<string name="notification_boost_description">Tilkynningar þegar færslurnar þínar eru endurbirtar</string>
|
||||
<string name="notification_favourite_name">Eftirlæti</string>
|
||||
<string name="notification_favourite_description">Tilkynningar þegar tístin þín eru sett í eftirlæti</string>
|
||||
<string name="notification_favourite_description">Tilkynningar þegar færslurnar þínar eru settar í eftirlæti</string>
|
||||
<string name="notification_poll_name">Kannanir</string>
|
||||
<string name="notification_poll_description">Tilkynningar um kannanir sem er lokið</string>
|
||||
<string name="notification_mention_format">%s minntist á þig</string>
|
||||
|
@ -273,7 +273,7 @@
|
|||
<string name="abbreviated_seconds_ago">%ds</string>
|
||||
<string name="follows_you">Fylgir þér</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Alltaf birta myndefni sem merkt er viðkvæmt</string>
|
||||
<string name="pref_title_alway_open_spoiler">Alltaf fletta út tístum sem eru með aðvörun vegna efnis</string>
|
||||
<string name="pref_title_alway_open_spoiler">Alltaf fletta út færslum sem eru með aðvörun vegna efnis</string>
|
||||
<string name="title_media">Gagnaskrár</string>
|
||||
<string name="replying_to">Svar til @%s</string>
|
||||
<string name="load_more_placeholder_text">hlaða inn fleiru</string>
|
||||
|
@ -376,7 +376,7 @@
|
|||
<string name="notifications_clear">Hreinsa</string>
|
||||
<string name="notifications_apply_filter">Sía</string>
|
||||
<string name="filter_apply">Virkja</string>
|
||||
<string name="compose_shortcut_long_label">Semja tíst</string>
|
||||
<string name="compose_shortcut_long_label">Semja færslu</string>
|
||||
<string name="compose_shortcut_short_label">Semja skilaboð</string>
|
||||
<string name="notification_clear_text">Ertu viss um að þú viljir endanlega eyða öllum tilkynningunum þínum\?</string>
|
||||
<string name="compose_preview_image_description">Aðgerðir fyrir mynd %s</string>
|
||||
|
@ -478,7 +478,7 @@
|
|||
<string name="wellbeing_mode_notice">Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á:
|
||||
\n
|
||||
\n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir
|
||||
\n - Eftirlæti/Talningu á endurbirtingum tísta
|
||||
\n - Eftirlæti/Talningu á endurbirtingum færslna
|
||||
\n - Fylgjendur/Tölfræði færslna í notendasniðum
|
||||
\n
|
||||
\n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar.</string>
|
||||
|
@ -503,9 +503,9 @@
|
|||
<string name="limit_notifications">Takmarka tilkynningar á tímalínu</string>
|
||||
<string name="review_notifications">Yfirfara tilkynningar</string>
|
||||
<string name="pref_title_wellbeing_mode">Vellíðan</string>
|
||||
<string name="notification_subscription_description">Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýtt tíst</string>
|
||||
<string name="notification_subscription_name">Ný tíst</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">einhver sem ég er áskrifandi að birti nýtt tíst</string>
|
||||
<string name="notification_subscription_description">Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýja færslu</string>
|
||||
<string name="notification_subscription_name">Nýjar færslur</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">einhver sem ég er áskrifandi að birti nýja færslu</string>
|
||||
<string name="notification_subscription_format">%s sendi inn rétt í þessu</string>
|
||||
<string name="follow_requests_info">Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum.</string>
|
||||
<string name="action_unbookmark">Fjarlægja bókamerki</string>
|
||||
|
@ -518,4 +518,5 @@
|
|||
<string name="duration_180_days">180 dagar</string>
|
||||
<string name="duration_365_days">365 dagar</string>
|
||||
<string name="duration_14_days">14 dagar</string>
|
||||
<string name="tusky_compose_post_quicksetting_label">Semja færslu</string>
|
||||
</resources>
|
|
@ -506,10 +506,10 @@
|
|||
<string name="limit_notifications">Ogranicz liczbę powiadomień o zmianach na osi czasu</string>
|
||||
<string name="label_duration">Czas trwania</string>
|
||||
<string name="notification_subscription_name">Nowe wpisy</string>
|
||||
<string name="wellbeing_mode_notice">Niektóre informacje, które mogą wpływać na Twoj dobrostan psychiczny zostaną ukryte. W ich skład wchodzą:
|
||||
<string name="wellbeing_mode_notice">Niektóre informacje, które mogą wpływać na Twój dobrostan psychiczny zostaną ukryte. W ich skład wchodzą:
|
||||
\n
|
||||
\n - powiadomienia o ulubionych/podbiciach/obserwowaniu
|
||||
\n - liczba polubień/podbić toota
|
||||
\n - liczba polubień/podbić wpisu
|
||||
\n - statystyki obserwujących/postów na profilach
|
||||
\n
|
||||
\nNie będzie to miało wpływu na powiadomienia typu push, ale możesz zmienić ustawienia powiadomień ręcznie.</string>
|
||||
|
@ -542,4 +542,11 @@
|
|||
<string name="notification_follow_request_format">%s poprosił(a) o możliwość śledzenia Cię</string>
|
||||
<string name="action_unbookmark">Usuń z zakładek</string>
|
||||
<string name="pref_title_confirm_favourites">Pytaj o potwierdzenie przed dodaniem do ulubionych</string>
|
||||
<string name="duration_14_days">14 dni</string>
|
||||
<string name="duration_30_days">30 dni</string>
|
||||
<string name="duration_60_days">60 dni</string>
|
||||
<string name="duration_90_days">90 dni</string>
|
||||
<string name="duration_180_days">180 dni</string>
|
||||
<string name="duration_365_days">365 dni</string>
|
||||
<string name="tusky_compose_post_quicksetting_label">Utwórz wpis</string>
|
||||
</resources>
|
|
@ -16,7 +16,7 @@
|
|||
<string name="title_notifications">Сповіщення</string>
|
||||
<string name="title_home">Головна</string>
|
||||
<string name="error_sender_account_gone">Помилка надсилання допису.</string>
|
||||
<string name="error_media_upload_image_or_video">Зображення та відео не можуть бути прикріплені до статусу одночасно.</string>
|
||||
<string name="error_media_upload_image_or_video">Зображення та відео не можуть бути прикріплені до допису одночасно.</string>
|
||||
<string name="error_media_download_permission">Потрібен дозвіл на зберігання медіа.</string>
|
||||
<string name="error_media_upload_permission">Потрібен дозвіл на читання медіа.</string>
|
||||
<string name="error_media_upload_opening">Не вдається відкрити цей файл.</string>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<string name="error_audio_upload_size">Аудіофайли повинні бути менше 40 МБ.</string>
|
||||
<string name="error_video_upload_size">Відео повинне бути менше 40 МБ.</string>
|
||||
<string name="error_image_upload_size">Файл повинен бути менше 8 МБ.</string>
|
||||
<string name="error_compose_character_limit">Статус надто довгий!</string>
|
||||
<string name="error_compose_character_limit">Допис задовгий!</string>
|
||||
<string name="error_no_web_browser_found">Не вдалося знайти браузер, який можна використати.</string>
|
||||
<string name="error_empty">Не може бути порожнім.</string>
|
||||
<string name="error_network">Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову!</string>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<resources>
|
||||
<string name="notification_clear_text">Bạn có muốn xóa toàn bộ thông báo\?</string>
|
||||
<string name="send_post_notification_saved_content">Đã lưu tút vào nháp</string>
|
||||
<string name="send_post_notification_cancel_title">Đã hủy đăng</string>
|
||||
<string name="send_post_notification_cancel_title">Hủy đăng</string>
|
||||
<string name="send_post_notification_channel_name">Đăng Tút</string>
|
||||
<string name="send_post_notification_title">Đang đăng…</string>
|
||||
<plurals name="notification_title_summary">
|
||||
|
@ -192,7 +192,7 @@
|
|||
<string name="title_posts_pinned">Ghim</string>
|
||||
<string name="title_posts_with_replies">Trả lời</string>
|
||||
<string name="title_posts">Tút</string>
|
||||
<string name="title_view_thread">Chuỗi tút</string>
|
||||
<string name="title_view_thread">Nội dung tút</string>
|
||||
<string name="title_tab_preferences">Xếp tab</string>
|
||||
<string name="title_direct_messages">Tin nhắn</string>
|
||||
<string name="title_public_federated">Thế giới</string>
|
||||
|
@ -394,7 +394,7 @@
|
|||
<string name="title_favourited_by">Những người thích tút này</string>
|
||||
<string name="title_reblogged_by">Những người đăng lại tút này</string>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="other"><b>%s</b> lượt đăng lại</item>
|
||||
<item quantity="other"><b>%s</b> Đăng lại</item>
|
||||
</plurals>
|
||||
<plurals name="favs">
|
||||
<item quantity="other"><b>%1$s</b> Thích</item>
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
<string name="notification_favourite_format">%s favorited your post</string>
|
||||
<string name="notification_follow_format">%s followed you</string>
|
||||
<string name="notification_follow_request_format">%s requested to follow you</string>
|
||||
<string name="notification_sign_up_format">%s signed up</string>
|
||||
<string name="notification_subscription_format">%s just posted</string>
|
||||
|
||||
<string name="report_username_format">Report @%s</string>
|
||||
|
@ -245,6 +246,7 @@
|
|||
<string name="pref_title_notification_filter_favourites">my posts are favorited</string>
|
||||
<string name="pref_title_notification_filter_poll">polls have ended</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">somebody I\'m subscribed to published a new post</string>
|
||||
<string name="pref_title_notification_filter_sign_ups">somebody signed up</string>
|
||||
<string name="pref_title_appearance_settings">Appearance</string>
|
||||
<string name="pref_title_app_theme">App Theme</string>
|
||||
<string name="pref_title_timelines">Timelines</string>
|
||||
|
@ -321,6 +323,8 @@
|
|||
<string name="notification_poll_description">Notifications about polls that have ended</string>
|
||||
<string name="notification_subscription_name">New posts</string>
|
||||
<string name="notification_subscription_description">Notifications when somebody you\'re subscribed to published a new post</string>
|
||||
<string name="notification_sign_up_name">Sign ups</string>
|
||||
<string name="notification_sign_up_description">Notifications about new users</string>
|
||||
|
||||
<string name="notification_mention_format">%s mentioned you</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>
|
||||
|
|
|
@ -15,16 +15,11 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.text.SpannedString
|
||||
import android.widget.LinearLayout
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
|
@ -39,8 +34,8 @@ import org.junit.runner.RunWith
|
|||
import org.junit.runners.Parameterized
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.mock
|
||||
import java.util.ArrayList
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -74,7 +69,7 @@ class BottomSheetActivityTest {
|
|||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
reblog = null,
|
||||
content = SpannedString("omgwat"),
|
||||
content = "omgwat",
|
||||
createdAt = Date(),
|
||||
emojis = emptyList(),
|
||||
reblogsCount = 0,
|
||||
|
@ -307,7 +302,7 @@ class BottomSheetActivityTest {
|
|||
init {
|
||||
mastodonApi = api
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
||||
bottomSheet = mock()
|
||||
}
|
||||
|
||||
override fun openLink(url: String) {
|
||||
|
|
|
@ -17,15 +17,12 @@ package com.keylesspalace.tusky
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Looper.getMainLooper
|
||||
import android.text.SpannedString
|
||||
import android.widget.EditText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
||||
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
||||
import com.keylesspalace.tusky.components.compose.MediaUploader
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
|
@ -37,18 +34,16 @@ import com.keylesspalace.tusky.entity.Instance
|
|||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleObserver
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
@ -94,44 +89,47 @@ class ComposeActivityTest {
|
|||
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
|
||||
activity = controller.get()
|
||||
|
||||
accountManagerMock = mock(AccountManager::class.java)
|
||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||
accountManagerMock = mock {
|
||||
on { activeAccount } doReturn account
|
||||
}
|
||||
|
||||
apiMock = mock(MastodonApi::class.java)
|
||||
`when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList()))
|
||||
`when`(apiMock.getInstance()).thenReturn(object : Single<Instance>() {
|
||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
||||
val instance = instanceResponseCallback?.invoke()
|
||||
apiMock = mock {
|
||||
on { getCustomEmojis() } doReturn Single.just(emptyList())
|
||||
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||
if (instance == null) {
|
||||
observer.onError(Throwable())
|
||||
Result.failure(Throwable())
|
||||
} else {
|
||||
observer.onSuccess(instance)
|
||||
Result.success(instance)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val instanceDaoMock = mock(InstanceDao::class.java)
|
||||
`when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn(
|
||||
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||
)
|
||||
val instanceDaoMock: InstanceDao = mock {
|
||||
on { loadMetadataForInstance(any()) } doReturn
|
||||
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||
on { loadMetadataForInstance(any()) } doReturn
|
||||
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||
}
|
||||
|
||||
val dbMock = mock(AppDatabase::class.java)
|
||||
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
||||
val dbMock: AppDatabase = mock {
|
||||
on { instanceDao() } doReturn instanceDaoMock
|
||||
}
|
||||
|
||||
val viewModel = ComposeViewModel(
|
||||
apiMock,
|
||||
accountManagerMock,
|
||||
mock(MediaUploader::class.java),
|
||||
mock(ServiceClient::class.java),
|
||||
mock(DraftHelper::class.java),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
dbMock
|
||||
)
|
||||
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
|
||||
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
||||
}
|
||||
|
||||
val viewModelFactoryMock = mock(ViewModelFactory::class.java)
|
||||
`when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel)
|
||||
val viewModelFactoryMock: ViewModelFactory = mock {
|
||||
on { create(ComposeViewModel::class.java) } doReturn viewModel
|
||||
}
|
||||
|
||||
activity.accountManager = accountManagerMock
|
||||
activity.viewModelFactory = viewModelFactoryMock
|
||||
|
@ -470,7 +468,7 @@ class ComposeActivityTest {
|
|||
"admin",
|
||||
"admin",
|
||||
"admin",
|
||||
SpannedString(""),
|
||||
"",
|
||||
"https://example.token",
|
||||
"",
|
||||
"",
|
||||
|
@ -490,7 +488,7 @@ class ComposeActivityTest {
|
|||
)
|
||||
}
|
||||
|
||||
fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
|
||||
private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
|
||||
return InstanceConfiguration(
|
||||
statuses = StatusConfiguration(
|
||||
maxCharacters = maximumStatusCharacters,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import android.text.SpannedString
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status(
|
|||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
reblog = null,
|
||||
content = SpannedString("Test"),
|
||||
content = "Test",
|
||||
createdAt = fixedDate,
|
||||
emojis = emptyList(),
|
||||
reblogsCount = 1,
|
||||
|
@ -51,7 +50,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete(
|
|||
status = mockStatus(id),
|
||||
isExpanded = false,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = true,
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Tusky v16.0
|
||||
|
||||
- Logika ładowania osi czasu została przepisana w celu przyspieszenia jej i naprawienia błędów.
|
||||
- Tusky wspiera teraz animowane emotikony w formatach APNG i Animated WebP.
|
||||
- Mnóstwo poprawek
|
||||
- Wsparcie dla Androida 11
|
||||
- Nowe tłumaczenia: Gaelicki szkocki, galicyjski, ukraiński
|
||||
- Ulepszone tłumaczenia
|
|
@ -0,0 +1,7 @@
|
|||
Tusky v17.0
|
||||
|
||||
- «Відкрити як...» тепер також доступно в меню профілів облікових записів за користування кількома обліковими записами
|
||||
- Тепер вхід обробляється у WebView у застосунку
|
||||
- Підтримка Android 12
|
||||
- підтримка нового API конфігурації сервера Mastodon
|
||||
- і багато інших дрібних виправлень і вдосконалень
|
Loading…
Reference in New Issue