Merge remote-tracking branch 'tuskyapp/develop'
# Conflicts: # app/build.gradle # app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt # app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java # app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt # app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt # app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java # app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt # app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt # app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt # app/src/main/res/layout/activity_main.xml # app/src/main/res/values-cs/strings.xml # app/src/main/res/values-de/strings.xml # app/src/main/res/values-fa/strings.xml # app/src/main/res/values-fr/strings.xml # app/src/main/res/values-hu/strings.xml # app/src/main/res/values-it/strings.xml # app/src/main/res/values-no-rNB/strings.xml # app/src/main/res/values-zh-rCN/strings.xml
This commit is contained in:
commit
57aab71b0e
|
@ -108,7 +108,7 @@ ext.glideVersion = '4.13.1'
|
||||||
ext.daggerVersion = '2.42'
|
ext.daggerVersion = '2.42'
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
ext.emoji2_version = '1.1.0'
|
ext.emoji2_version = '1.1.0'
|
||||||
ext.filemojicompat_version = '3.2.1'
|
ext.filemojicompat_version = '3.2.2'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
|
@ -154,7 +154,7 @@ dependencies {
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
||||||
implementation "at.connyduck:kotlin-result-calladapter:1.0.1"
|
implementation "at.connyduck:networkresult-calladapter:1.0.0"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||||
|
@ -165,7 +165,7 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
|
|
||||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0"
|
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.22.0"
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
|
@ -188,7 +188,7 @@ dependencies {
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||||
|
|
||||||
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
implementation "com.github.CanHub:Android-Image-Cropper:4.2.1"
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
|
implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
|
||||||
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
||||||
|
@ -202,6 +202,8 @@ dependencies {
|
||||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
||||||
|
|
||||||
|
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
|
|
|
@ -0,0 +1,869 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 37,
|
||||||
|
"identityHash": "11033751d382aa8a1c6fc68833097d35",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` 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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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, '11033751d382aa8a1c6fc68833097d35')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,875 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 38,
|
||||||
|
"identityHash": "798fc8d34064eb671c079689d4650ea5",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER 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_repliesCount` 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": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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, '798fc8d34064eb671c079689d4650ea5')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,893 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 39,
|
||||||
|
"identityHash": "bf4c9d8417b71e549170a568522d513d",
|
||||||
|
"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, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `quote` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quote",
|
||||||
|
"columnName": "quote",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER 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_repliesCount` 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": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"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, 'bf4c9d8417b71e549170a568522d513d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import autodispose2.androidx.lifecycle.autoDispose
|
import autodispose2.androidx.lifecycle.autoDispose
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
@ -70,13 +71,10 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
|
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
|
||||||
import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount
|
|
||||||
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
|
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
|
||||||
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
|
@ -94,11 +92,12 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.ResettableFragment
|
import com.keylesspalace.tusky.interfaces.ResettableFragment
|
||||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.usecase.LogoutUsecase
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.removeShortcut
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.updateShortcut
|
import com.keylesspalace.tusky.util.updateShortcut
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
@ -153,10 +152,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
lateinit var cacheUpdater: CacheUpdater
|
lateinit var cacheUpdater: CacheUpdater
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var conversationRepository: ConversationsRepository
|
lateinit var logoutUsecase: LogoutUsecase
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var draftHelper: DraftHelper
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
@ -834,28 +830,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
.setTitle(R.string.action_logout)
|
.setTitle(R.string.action_logout)
|
||||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
binding.appBar.hide()
|
||||||
|
binding.viewPager.hide()
|
||||||
|
binding.progressBar.show()
|
||||||
|
binding.bottomNav.hide()
|
||||||
|
binding.composeButton.hide()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Only disable UnifiedPush for this account -- do not call disableNotifications(),
|
val otherAccountAvailable = logoutUsecase.logout()
|
||||||
// which unnecessarily disables it for all accounts and then re-enables it again at
|
val intent = if (otherAccountAvailable) {
|
||||||
// the next launch
|
|
||||||
disableUnifiedPushNotificationsForAccount(this@MainActivity, activeAccount)
|
|
||||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
|
||||||
cacheUpdater.clearForUser(activeAccount.id)
|
|
||||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
|
||||||
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
|
||||||
removeShortcut(this@MainActivity, activeAccount)
|
|
||||||
val newAccount = accountManager.logActiveAccountOut()
|
|
||||||
if (!NotificationHelper.areNotificationsEnabled(
|
|
||||||
this@MainActivity,
|
|
||||||
accountManager
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
|
||||||
}
|
|
||||||
val intent = if (newAccount == null) {
|
|
||||||
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
|
||||||
} else {
|
|
||||||
Intent(this@MainActivity, MainActivity::class.java)
|
Intent(this@MainActivity, MainActivity::class.java)
|
||||||
|
} else {
|
||||||
|
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finishWithoutSlideOutAnimation()
|
||||||
|
@ -888,7 +874,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
||||||
|
|
||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
showMigrationNoticeIfNecessary(this, binding.root, accountManager)
|
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager)
|
||||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
|
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
||||||
|
@ -37,12 +36,8 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(applicationContext))
|
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
|
/** Determine whether the user is currently logged in, and if so go ahead and load the
|
||||||
* timeline. Otherwise, start the activity_login screen. */
|
* timeline. Otherwise, start the activity_login screen. */
|
||||||
|
|
||||||
val intent = if (accountManager.activeAccount != null) {
|
val intent = if (accountManager.activeAccount != null) {
|
||||||
Intent(this, MainActivity::class.java)
|
Intent(this, MainActivity::class.java)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
import autodispose2.autoDispose
|
import autodispose2.autoDispose
|
||||||
|
|
|
@ -44,6 +44,7 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
||||||
|
|
||||||
binding.username.text = account.fullName
|
binding.username.text = account.fullName
|
||||||
binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis)
|
binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis)
|
||||||
|
binding.avatarBadge.visibility = View.GONE // We never want to display the bot badge here
|
||||||
|
|
||||||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
||||||
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/* Copyright 2019 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.adapter
|
|
||||||
|
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
|
||||||
import com.keylesspalace.tusky.util.visible
|
|
||||||
|
|
||||||
class NetworkStateViewHolder(
|
|
||||||
private val binding: ItemNetworkStateBinding,
|
|
||||||
private val retryCallback: () -> Unit
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun setUpWithNetworkState(state: LoadState) {
|
|
||||||
binding.progressBar.visible(state == LoadState.Loading)
|
|
||||||
binding.retryButton.visible(state is LoadState.Error)
|
|
||||||
val msg = if (state is LoadState.Error) {
|
|
||||||
state.error.message
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
binding.errorMsg.visible(msg != null)
|
|
||||||
binding.errorMsg.text = msg
|
|
||||||
binding.retryButton.setOnClickListener {
|
|
||||||
retryCallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -74,10 +74,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
public static class Key {
|
public static class Key {
|
||||||
public static final String KEY_CREATED = "created";
|
public static final String KEY_CREATED = "created";
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextView displayName;
|
private TextView displayName;
|
||||||
private TextView username;
|
private TextView username;
|
||||||
private ImageButton replyButton;
|
private ImageButton replyButton;
|
||||||
|
private TextView replyCountLabel;
|
||||||
private SparkButton reblogButton;
|
private SparkButton reblogButton;
|
||||||
private SparkButton favouriteButton;
|
private SparkButton favouriteButton;
|
||||||
private ImageButton quoteButton;
|
private ImageButton quoteButton;
|
||||||
|
@ -129,6 +129,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
content = itemView.findViewById(R.id.status_content);
|
content = itemView.findViewById(R.id.status_content);
|
||||||
avatar = itemView.findViewById(R.id.status_avatar);
|
avatar = itemView.findViewById(R.id.status_avatar);
|
||||||
replyButton = itemView.findViewById(R.id.status_reply);
|
replyButton = itemView.findViewById(R.id.status_reply);
|
||||||
|
replyCountLabel = itemView.findViewById(R.id.status_replies);
|
||||||
reblogButton = itemView.findViewById(R.id.status_inset);
|
reblogButton = itemView.findViewById(R.id.status_inset);
|
||||||
favouriteButton = itemView.findViewById(R.id.status_favourite);
|
favouriteButton = itemView.findViewById(R.id.status_favourite);
|
||||||
quoteButton = itemView.findViewById(R.id.status_quote);
|
quoteButton = itemView.findViewById(R.id.status_quote);
|
||||||
|
@ -418,6 +419,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setReplyCount(int repliesCount) {
|
||||||
|
// This label only exists in the non-detailed view (to match the web ui)
|
||||||
|
if (replyCountLabel != null) {
|
||||||
|
replyCountLabel.setText((repliesCount > 1 ? replyCountLabel.getContext().getString(R.string.status_count_one_plus) : Integer.toString(repliesCount)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setReblogged(boolean reblogged) {
|
private void setReblogged(boolean reblogged) {
|
||||||
reblogButton.setChecked(reblogged);
|
reblogButton.setChecked(reblogged);
|
||||||
}
|
}
|
||||||
|
@ -841,6 +849,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||||
setStatusVisibility(actionable.getVisibility());
|
setStatusVisibility(actionable.getVisibility());
|
||||||
setIsReply(actionable.getInReplyToId() != null);
|
setIsReply(actionable.getInReplyToId() != null);
|
||||||
|
setReplyCount(actionable.getRepliesCount());
|
||||||
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
||||||
actionable.getAccount().getBot(), statusDisplayOptions);
|
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||||
setReblogged(actionable.getReblogged());
|
setReblogged(actionable.getReblogged());
|
||||||
|
@ -1147,6 +1156,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
actionable.getPoll() == null &&
|
actionable.getPoll() == null &&
|
||||||
card != null &&
|
card != null &&
|
||||||
!TextUtils.isEmpty(card.getUrl()) &&
|
!TextUtils.isEmpty(card.getUrl()) &&
|
||||||
|
(!actionable.getSensitive() || status.isExpanded()) &&
|
||||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||||
cardView.setVisibility(View.VISIBLE);
|
cardView.setVisibility(View.VISIBLE);
|
||||||
cardTitle.setText(card.getTitle());
|
cardTitle.setText(card.getTitle());
|
||||||
|
|
|
@ -107,18 +107,24 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
@NonNull final StatusActionListener listener,
|
@NonNull final StatusActionListener listener,
|
||||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
// We never collapse statuses in the detail view
|
||||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
|
||||||
|
status.copyWithCollapsed(false) :
|
||||||
|
status;
|
||||||
|
|
||||||
|
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
|
||||||
|
setupCard(uncollapsedStatus, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
|
Status actionable = uncollapsedStatus.getActionable();
|
||||||
|
|
||||||
if (!statusDisplayOptions.hideStats()) {
|
if (!statusDisplayOptions.hideStats()) {
|
||||||
setReblogAndFavCount(status.getActionable().getReblogsCount(),
|
setReblogAndFavCount(actionable.getReblogsCount(),
|
||||||
status.getActionable().getFavouritesCount(), listener);
|
actionable.getFavouritesCount(), listener);
|
||||||
} else {
|
} else {
|
||||||
hideQuantitativeStats();
|
hideQuantitativeStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
setApplication(status.getActionable().getApplication());
|
setApplication(actionable.getApplication());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import javax.inject.Inject
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val appDatabase: AppDatabase,
|
appDatabase: AppDatabase,
|
||||||
gson: Gson
|
gson: Gson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -44,8 +44,4 @@ class CacheUpdater @Inject constructor(
|
||||||
fun stop() {
|
fun stop() {
|
||||||
this.disposable.dispose()
|
this.disposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearForUser(accountId: Long) {
|
|
||||||
appDatabase.timelineDao().removeAll(accountId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
|
@ -58,6 +59,9 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
import com.canhub.cropper.CropImage
|
||||||
|
import com.canhub.cropper.CropImageContract
|
||||||
|
import com.canhub.cropper.options
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
@ -87,6 +91,7 @@ import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.afterTextChanged
|
import com.keylesspalace.tusky.util.afterTextChanged
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||||
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.highlightSpans
|
import com.keylesspalace.tusky.util.highlightSpans
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -157,6 +162,32 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contract kicked off by editImageInQueue; expects viewModel.cropImageItemOld set
|
||||||
|
private val cropImage = registerForActivityResult(CropImageContract()) { result ->
|
||||||
|
val uriNew = result.uriContent
|
||||||
|
if (result.isSuccessful && uriNew != null) {
|
||||||
|
viewModel.cropImageItemOld?.let { itemOld ->
|
||||||
|
val size = getMediaSize(contentResolver, uriNew)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.addMediaToQueue(
|
||||||
|
itemOld.type,
|
||||||
|
uriNew,
|
||||||
|
size,
|
||||||
|
itemOld.description,
|
||||||
|
itemOld
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (result == CropImage.CancelledResult) {
|
||||||
|
Log.w("ComposeActivity", "Edit image cancelled by user")
|
||||||
|
} else {
|
||||||
|
Log.w("ComposeActivity", "Edit image failed: " + result.error)
|
||||||
|
displayTransientError(R.string.error_image_edit_failed)
|
||||||
|
}
|
||||||
|
viewModel.cropImageItemOld = null
|
||||||
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -191,6 +222,7 @@ class ComposeActivity :
|
||||||
viewModel.updateDescription(item.localId, newDescription)
|
viewModel.updateDescription(item.localId, newDescription)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onEditImage = this::editImageInQueue,
|
||||||
onRemove = this::removeMediaFromQueue
|
onRemove = this::removeMediaFromQueue
|
||||||
)
|
)
|
||||||
binding.composeMediaPreviewBar.layoutManager =
|
binding.composeMediaPreviewBar.layoutManager =
|
||||||
|
@ -429,8 +461,13 @@ class ComposeActivity :
|
||||||
enableButton(binding.composeAddMediaButton, active, active)
|
enableButton(binding.composeAddMediaButton, active, active)
|
||||||
enablePollButton(media.isNullOrEmpty())
|
enablePollButton(media.isNullOrEmpty())
|
||||||
}.subscribe()
|
}.subscribe()
|
||||||
viewModel.uploadError.observe {
|
viewModel.uploadError.observe { throwable ->
|
||||||
displayTransientError(R.string.error_media_upload_sending)
|
Log.w(TAG, "media upload failed", throwable)
|
||||||
|
if (throwable is UploadServerError) {
|
||||||
|
displayTransientError(throwable.errorMessage)
|
||||||
|
} else {
|
||||||
|
displayTransientError(R.string.error_media_upload_sending)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
viewModel.setupComplete.observe {
|
viewModel.setupComplete.observe {
|
||||||
// Focus may have changed during view model setup, ensure initial focus is on the edit field
|
// Focus may have changed during view model setup, ensure initial focus is on the edit field
|
||||||
|
@ -594,19 +631,23 @@ class ComposeActivity :
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayTransientError(@StringRes stringId: Int) {
|
private fun displayTransientError(errorMessage: String) {
|
||||||
val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG)
|
val bar = Snackbar.make(binding.activityCompose, errorMessage, Snackbar.LENGTH_LONG)
|
||||||
// necessary so snackbar is shown over everything
|
// necessary so snackbar is shown over everything
|
||||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||||
|
bar.setAnchorView(R.id.composeBottomBar)
|
||||||
bar.show()
|
bar.show()
|
||||||
}
|
}
|
||||||
|
private fun displayTransientError(@StringRes stringId: Int) {
|
||||||
|
displayTransientError(getString(stringId))
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleHideMedia() {
|
private fun toggleHideMedia() {
|
||||||
this.viewModel.toggleMarkSensitive()
|
this.viewModel.toggleMarkSensitive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
|
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
|
||||||
if (viewModel.media.value.isNullOrEmpty()) {
|
if (viewModel.media.value.isEmpty()) {
|
||||||
binding.composeHideMediaButton.hide()
|
binding.composeHideMediaButton.hide()
|
||||||
} else {
|
} else {
|
||||||
binding.composeHideMediaButton.show()
|
binding.composeHideMediaButton.show()
|
||||||
|
@ -948,6 +989,26 @@ class ComposeActivity :
|
||||||
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun editImageInQueue(item: QueuedMedia) {
|
||||||
|
// If input image is lossless, output image should be lossless.
|
||||||
|
// Currently the only supported lossless format is png.
|
||||||
|
val mimeType: String? = contentResolver.getType(item.uri)
|
||||||
|
val isPng: Boolean = mimeType != null && mimeType.endsWith("/png")
|
||||||
|
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
|
||||||
|
|
||||||
|
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
|
||||||
|
val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
|
||||||
|
|
||||||
|
viewModel.cropImageItemOld = item
|
||||||
|
|
||||||
|
cropImage.launch(
|
||||||
|
options(uri = item.uri) {
|
||||||
|
setOutputUri(uriNew)
|
||||||
|
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeMediaFromQueue(item: QueuedMedia) {
|
private fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
viewModel.removeMediaFromQueue(item)
|
viewModel.removeMediaFromQueue(item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
|
@ -39,7 +40,6 @@ import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.StatusToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import com.keylesspalace.tusky.util.result
|
|
||||||
import com.keylesspalace.tusky.util.toLiveData
|
import com.keylesspalace.tusky.util.toLiveData
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -100,6 +100,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
||||||
|
|
||||||
|
// Used in ComposeActivity to pass state to result function when cropImage contract inflight
|
||||||
|
var cropImageItemOld: QueuedMedia? = null
|
||||||
|
|
||||||
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
|
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
emoji.postValue(when (loadActually) {
|
emoji.postValue(when (loadActually) {
|
||||||
|
@ -133,13 +136,16 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addMediaToQueue(
|
suspend fun addMediaToQueue(
|
||||||
type: QueuedMedia.Type,
|
type: QueuedMedia.Type,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mediaSize: Long,
|
mediaSize: Long,
|
||||||
description: String? = null
|
description: String? = null,
|
||||||
|
replaceItem: QueuedMedia? = null
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
val mediaItem = media.updateAndGet { mediaValue ->
|
var stashMediaItem: QueuedMedia? = null
|
||||||
|
|
||||||
|
media.updateAndGet { mediaValue ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
@ -147,8 +153,19 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaSize = mediaSize,
|
mediaSize = mediaSize,
|
||||||
description = description
|
description = description
|
||||||
)
|
)
|
||||||
mediaValue + mediaItem
|
stashMediaItem = mediaItem
|
||||||
}.last()
|
|
||||||
|
if (replaceItem != null) {
|
||||||
|
mediaToJob[replaceItem.localId]?.cancel()
|
||||||
|
mediaValue.map {
|
||||||
|
if (it.localId == replaceItem.localId) mediaItem else it
|
||||||
|
}
|
||||||
|
} else { // Append
|
||||||
|
mediaValue + mediaItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that
|
||||||
|
|
||||||
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
.uploadMedia(mediaItem)
|
.uploadMedia(mediaItem)
|
||||||
|
@ -213,7 +230,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
val contentWarningChanged = showContentWarning.value!! &&
|
val contentWarningChanged = showContentWarning.value!! &&
|
||||||
!contentWarning.isNullOrEmpty() &&
|
!contentWarning.isNullOrEmpty() &&
|
||||||
!startingContentWarning.startsWith(contentWarning.toString())
|
!startingContentWarning.startsWith(contentWarning.toString())
|
||||||
val mediaChanged = !media.value.isNullOrEmpty()
|
val mediaChanged = media.value.isNotEmpty()
|
||||||
val pollChanged = poll.value != null
|
val pollChanged = poll.value != null
|
||||||
|
|
||||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged
|
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged
|
||||||
|
@ -346,8 +363,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||||
when (token[0]) {
|
when (token[0]) {
|
||||||
'@' -> {
|
'@' -> {
|
||||||
return api.searchAccountsCall(query = token.substring(1), limit = 10)
|
return api.searchAccountsSync(query = token.substring(1), limit = 10)
|
||||||
.result()
|
|
||||||
.fold({ accounts ->
|
.fold({ accounts ->
|
||||||
accounts.map { AutocompleteResult.AccountResult(it) }
|
accounts.map { AutocompleteResult.AccountResult(it) }
|
||||||
}, { e ->
|
}, { e ->
|
||||||
|
@ -356,8 +372,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
'#' -> {
|
'#' -> {
|
||||||
return api.searchCall(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||||
.result()
|
|
||||||
.fold({ searchResult ->
|
.fold({ searchResult ->
|
||||||
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
||||||
}, { e ->
|
}, { e ->
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.components.compose.view.ProgressImageView
|
||||||
class MediaPreviewAdapter(
|
class MediaPreviewAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
|
private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
||||||
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
||||||
|
|
||||||
|
@ -43,12 +44,16 @@ class MediaPreviewAdapter(
|
||||||
val item = differ.currentList[position]
|
val item = differ.currentList[position]
|
||||||
val popup = PopupMenu(view.context, view)
|
val popup = PopupMenu(view.context, view)
|
||||||
val addCaptionId = 1
|
val addCaptionId = 1
|
||||||
val removeId = 2
|
val editImageId = 2
|
||||||
|
val removeId = 3
|
||||||
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||||
|
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE)
|
||||||
|
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
||||||
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
||||||
popup.setOnMenuItemClickListener { menuItem ->
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
addCaptionId -> onAddCaption(item)
|
addCaptionId -> onAddCaption(item)
|
||||||
|
editImageId -> onEditImage(item)
|
||||||
removeId -> onRemove(item)
|
removeId -> onRemove(item)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.util.Log
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
@ -31,6 +32,7 @@ import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||||
import com.keylesspalace.tusky.util.getMediaSize
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
|
import com.keylesspalace.tusky.util.getServerErrorMessage
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -54,14 +56,14 @@ sealed class UploadEvent {
|
||||||
data class FinishedEvent(val mediaId: String) : UploadEvent()
|
data class FinishedEvent(val mediaId: String) : UploadEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNewImageFile(context: Context): File {
|
fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
|
||||||
// Create an image file name
|
// Create an image file name
|
||||||
val randomId = randomAlphanumericString(12)
|
val randomId = randomAlphanumericString(12)
|
||||||
val imageFileName = "Tusky_${randomId}_"
|
val imageFileName = "Tusky_${randomId}_"
|
||||||
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
return File.createTempFile(
|
return File.createTempFile(
|
||||||
imageFileName, /* prefix */
|
imageFileName, /* prefix */
|
||||||
".jpg", /* suffix */
|
suffix, /* suffix */
|
||||||
storageDir /* directory */
|
storageDir /* directory */
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -72,6 +74,7 @@ class AudioSizeException : Exception()
|
||||||
class VideoSizeException : Exception()
|
class VideoSizeException : Exception()
|
||||||
class MediaTypeException : Exception()
|
class MediaTypeException : Exception()
|
||||||
class CouldNotOpenFileException : Exception()
|
class CouldNotOpenFileException : Exception()
|
||||||
|
class UploadServerError(val errorMessage: String) : Exception()
|
||||||
|
|
||||||
class MediaUploader @Inject constructor(
|
class MediaUploader @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -222,13 +225,21 @@ class MediaUploader @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = mediaUploadApi.uploadMedia(body, description).getOrThrow()
|
mediaUploadApi.uploadMedia(body, description).fold({ result ->
|
||||||
if (media.uri.scheme == "file") {
|
if (media.uri.scheme == "file") {
|
||||||
media.uri.path?.let {
|
media.uri.path?.let {
|
||||||
File(it).delete()
|
File(it).delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
send(UploadEvent.FinishedEvent(result.id))
|
||||||
send(UploadEvent.FinishedEvent(result.id))
|
}, { throwable ->
|
||||||
|
val errorMessage = throwable.getServerErrorMessage()
|
||||||
|
if (errorMessage == null) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
throw UploadServerError(errorMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
awaitClose()
|
awaitClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,7 +256,7 @@ class MediaUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val TAG = "MediaUploaderImpl"
|
private const val TAG = "MediaUploader"
|
||||||
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
|
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
|
||||||
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
||||||
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
||||||
|
|
|
@ -20,21 +20,40 @@ import android.view.ViewGroup
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
|
||||||
class ConversationAdapter(
|
class ConversationAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private var statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val listener: StatusActionListener
|
private val listener: StatusActionListener
|
||||||
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||||
|
|
||||||
|
var mediaPreviewEnabled: Boolean
|
||||||
|
get() = statusDisplayOptions.mediaPreviewEnabled
|
||||||
|
set(mediaPreviewEnabled) {
|
||||||
|
statusDisplayOptions = statusDisplayOptions.copy(
|
||||||
|
mediaPreviewEnabled = mediaPreviewEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||||
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
|
||||||
holder.setupWithConversation(getItem(position))
|
onBindViewHolder(holder, position, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(
|
||||||
|
holder: ConversationViewHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any>
|
||||||
|
) {
|
||||||
|
getItem(position)?.let { conversationViewData ->
|
||||||
|
holder.setupWithConversation(conversationViewData, payloads.firstOrNull())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -44,7 +63,17 @@ class ConversationAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||||
return oldItem == newItem
|
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? {
|
||||||
|
return if (oldItem == newItem) {
|
||||||
|
// If items are equal - update timestamp only
|
||||||
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||||
|
} else {
|
||||||
|
// If items are different - update the whole view holder
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import java.util.Date
|
||||||
data class ConversationEntity(
|
data class ConversationEntity(
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
val id: String,
|
val id: String,
|
||||||
|
val order: Int,
|
||||||
val accounts: List<ConversationAccountEntity>,
|
val accounts: List<ConversationAccountEntity>,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||||
|
@ -41,6 +42,7 @@ data class ConversationEntity(
|
||||||
fun toViewData(): ConversationViewData {
|
fun toViewData(): ConversationViewData {
|
||||||
return ConversationViewData(
|
return ConversationViewData(
|
||||||
id = id,
|
id = id,
|
||||||
|
order = order,
|
||||||
accounts = accounts,
|
accounts = accounts,
|
||||||
unread = unread,
|
unread = unread,
|
||||||
lastStatus = lastStatus.toViewData()
|
lastStatus = lastStatus.toViewData()
|
||||||
|
@ -50,6 +52,7 @@ data class ConversationEntity(
|
||||||
|
|
||||||
data class ConversationAccountEntity(
|
data class ConversationAccountEntity(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
val localUsername: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
|
@ -58,12 +61,12 @@ data class ConversationAccountEntity(
|
||||||
fun toAccount(): TimelineAccount {
|
fun toAccount(): TimelineAccount {
|
||||||
return TimelineAccount(
|
return TimelineAccount(
|
||||||
id = id,
|
id = id,
|
||||||
|
localUsername = localUsername,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
url = "",
|
url = "",
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
localUsername = "",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +82,7 @@ data class ConversationStatusEntity(
|
||||||
val createdAt: Date,
|
val createdAt: Date,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
val favouritesCount: Int,
|
val favouritesCount: Int,
|
||||||
|
val repliesCount: Int,
|
||||||
val favourited: Boolean,
|
val favourited: Boolean,
|
||||||
val bookmarked: Boolean,
|
val bookmarked: Boolean,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
|
@ -107,6 +111,7 @@ data class ConversationStatusEntity(
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
favouritesCount = favouritesCount,
|
favouritesCount = favouritesCount,
|
||||||
|
repliesCount = repliesCount,
|
||||||
reblogged = false,
|
reblogged = false,
|
||||||
favourited = favourited,
|
favourited = favourited,
|
||||||
bookmarked = bookmarked,
|
bookmarked = bookmarked,
|
||||||
|
@ -133,6 +138,7 @@ data class ConversationStatusEntity(
|
||||||
fun TimelineAccount.toEntity() =
|
fun TimelineAccount.toEntity() =
|
||||||
ConversationAccountEntity(
|
ConversationAccountEntity(
|
||||||
id = id,
|
id = id,
|
||||||
|
localUsername = localUsername,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = name,
|
displayName = name,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
|
@ -150,6 +156,7 @@ fun Status.toEntity() =
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
favouritesCount = favouritesCount,
|
favouritesCount = favouritesCount,
|
||||||
|
repliesCount = repliesCount,
|
||||||
favourited = favourited,
|
favourited = favourited,
|
||||||
bookmarked = bookmarked,
|
bookmarked = bookmarked,
|
||||||
sensitive = sensitive,
|
sensitive = sensitive,
|
||||||
|
@ -164,10 +171,11 @@ fun Status.toEntity() =
|
||||||
poll = poll
|
poll = poll
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Conversation.toEntity(accountId: Long) =
|
fun Conversation.toEntity(accountId: Long, order: Int) =
|
||||||
ConversationEntity(
|
ConversationEntity(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
id = id,
|
id = id,
|
||||||
|
order = order,
|
||||||
accounts = accounts.map { it.toEntity() },
|
accounts = accounts.map { it.toEntity() },
|
||||||
unread = unread,
|
unread = unread,
|
||||||
lastStatus = lastStatus!!.toEntity()
|
lastStatus = lastStatus!!.toEntity()
|
||||||
|
|
|
@ -19,22 +19,35 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.LoadStateAdapter
|
import androidx.paging.LoadStateAdapter
|
||||||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
|
||||||
class ConversationLoadStateAdapter(
|
class ConversationLoadStateAdapter(
|
||||||
private val retryCallback: () -> Unit
|
private val retryCallback: () -> Unit
|
||||||
) : LoadStateAdapter<NetworkStateViewHolder>() {
|
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) {
|
||||||
holder.setUpWithNetworkState(loadState)
|
val binding = holder.binding
|
||||||
|
binding.progressBar.visible(loadState == LoadState.Loading)
|
||||||
|
binding.retryButton.visible(loadState is LoadState.Error)
|
||||||
|
val msg = if (loadState is LoadState.Error) {
|
||||||
|
loadState.error.message
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
binding.errorMsg.visible(msg != null)
|
||||||
|
binding.errorMsg.text = msg
|
||||||
|
binding.retryButton.setOnClickListener {
|
||||||
|
retryCallback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loadState: LoadState
|
loadState: LoadState
|
||||||
): NetworkStateViewHolder {
|
): BindingHolder<ItemNetworkStateBinding> {
|
||||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return NetworkStateViewHolder(binding, retryCallback)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
data class ConversationViewData(
|
data class ConversationViewData(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
val order: Int,
|
||||||
val accounts: List<ConversationAccountEntity>,
|
val accounts: List<ConversationAccountEntity>,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
val lastStatus: StatusViewData.Concrete
|
val lastStatus: StatusViewData.Concrete
|
||||||
|
@ -37,6 +38,7 @@ data class ConversationViewData(
|
||||||
return ConversationEntity(
|
return ConversationEntity(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
id = id,
|
id = id,
|
||||||
|
order = order,
|
||||||
accounts = accounts,
|
accounts = accounts,
|
||||||
unread = unread,
|
unread = unread,
|
||||||
lastStatus = lastStatus.toConversationStatusEntity(
|
lastStatus = lastStatus.toConversationStatusEntity(
|
||||||
|
@ -71,6 +73,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
createdAt = status.createdAt,
|
createdAt = status.createdAt,
|
||||||
emojis = status.emojis,
|
emojis = status.emojis,
|
||||||
favouritesCount = status.favouritesCount,
|
favouritesCount = status.favouritesCount,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
favourited = favourited,
|
favourited = favourited,
|
||||||
bookmarked = bookmarked,
|
bookmarked = bookmarked,
|
||||||
sensitive = status.sensitive,
|
sensitive = status.sensitive,
|
||||||
|
|
|
@ -23,6 +23,8 @@ import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
@ -43,12 +45,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||||
|
|
||||||
private TextView conversationNameTextView;
|
private final TextView conversationNameTextView;
|
||||||
private Button contentCollapseButton;
|
private final Button contentCollapseButton;
|
||||||
private ImageView[] avatars;
|
private final ImageView[] avatars;
|
||||||
|
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
private final StatusDisplayOptions statusDisplayOptions;
|
||||||
private StatusActionListener listener;
|
private final StatusActionListener listener;
|
||||||
|
|
||||||
ConversationViewHolder(View itemView,
|
ConversationViewHolder(View itemView,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
|
@ -64,7 +66,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
|
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -72,52 +73,67 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithConversation(ConversationViewData conversation) {
|
void setupWithConversation(
|
||||||
|
@NonNull ConversationViewData conversation,
|
||||||
|
@Nullable Object payloads
|
||||||
|
) {
|
||||||
|
|
||||||
StatusViewData.Concrete statusViewData = conversation.getLastStatus();
|
StatusViewData.Concrete statusViewData = conversation.getLastStatus();
|
||||||
Status status = statusViewData.getStatus();
|
Status status = statusViewData.getStatus();
|
||||||
TimelineAccount account = status.getAccount();
|
|
||||||
|
|
||||||
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
|
if (payloads == null) {
|
||||||
|
TimelineAccount account = status.getAccount();
|
||||||
|
|
||||||
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
|
||||||
setUsername(account.getUsername());
|
|
||||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
|
||||||
setIsReply(status.getInReplyToId() != null);
|
|
||||||
setFavourited(status.getFavourited());
|
|
||||||
setBookmarked(status.getBookmarked());
|
|
||||||
List<Attachment> attachments = status.getAttachments();
|
|
||||||
boolean sensitive = status.getSensitive();
|
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
|
||||||
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
|
||||||
statusDisplayOptions.useBlurhash());
|
|
||||||
|
|
||||||
if (attachments.size() == 0) {
|
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
||||||
|
setUsername(account.getUsername());
|
||||||
|
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||||
|
setIsReply(status.getInReplyToId() != null);
|
||||||
|
setFavourited(status.getFavourited());
|
||||||
|
setBookmarked(status.getBookmarked());
|
||||||
|
List<Attachment> attachments = status.getAttachments();
|
||||||
|
boolean sensitive = status.getSensitive();
|
||||||
|
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||||
|
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||||
|
statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
|
if (attachments.size() == 0) {
|
||||||
|
hideSensitiveMediaWarning();
|
||||||
|
}
|
||||||
|
// Hide the unused label.
|
||||||
|
for (TextView mediaLabel : mediaLabels) {
|
||||||
|
mediaLabel.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
||||||
|
// Hide all unused views.
|
||||||
|
mediaPreviews[0].setVisibility(View.GONE);
|
||||||
|
mediaPreviews[1].setVisibility(View.GONE);
|
||||||
|
mediaPreviews[2].setVisibility(View.GONE);
|
||||||
|
mediaPreviews[3].setVisibility(View.GONE);
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
// Hide the unused label.
|
|
||||||
for (TextView mediaLabel : mediaLabels) {
|
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
||||||
mediaLabel.setVisibility(View.GONE);
|
false, statusDisplayOptions);
|
||||||
}
|
|
||||||
|
setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
|
||||||
|
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||||
|
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||||
|
|
||||||
|
setConversationName(conversation.getAccounts());
|
||||||
|
|
||||||
|
setAvatars(conversation.getAccounts());
|
||||||
} else {
|
} else {
|
||||||
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
if (payloads instanceof List) {
|
||||||
// Hide all unused views.
|
for (Object item : (List<?>) payloads) {
|
||||||
mediaPreviews[0].setVisibility(View.GONE);
|
if (Key.KEY_CREATED.equals(item)) {
|
||||||
mediaPreviews[1].setVisibility(View.GONE);
|
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||||
mediaPreviews[2].setVisibility(View.GONE);
|
}
|
||||||
mediaPreviews[3].setVisibility(View.GONE);
|
}
|
||||||
hideSensitiveMediaWarning();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
|
||||||
false, statusDisplayOptions);
|
|
||||||
|
|
||||||
setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
|
|
||||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
|
||||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
|
||||||
|
|
||||||
setConversationName(conversation.getAccounts());
|
|
||||||
|
|
||||||
setAvatars(conversation.getAccounts());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||||
|
|
|
@ -22,21 +22,28 @@ import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.ExperimentalPagingApi
|
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import autodispose2.androidx.lifecycle.autoDispose
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.StatusListActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository.Companion.CAN_USE_QUOTE_ID
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository.Companion.CAN_USE_QUOTE_ID
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.fragment.SFragment
|
import com.keylesspalace.tusky.fragment.SFragment
|
||||||
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -45,29 +52,31 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
|
||||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
|
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||||
|
|
||||||
private lateinit var adapter: ConversationAdapter
|
private lateinit var adapter: ConversationAdapter
|
||||||
private lateinit var loadStateAdapter: ConversationLoadStateAdapter
|
|
||||||
|
|
||||||
private var layoutManager: LinearLayoutManager? = null
|
private var hideFab = false
|
||||||
|
|
||||||
private var initialRefreshDone: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||||
|
@ -91,56 +100,106 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter = ConversationAdapter(statusDisplayOptions, this)
|
adapter = ConversationAdapter(statusDisplayOptions, this)
|
||||||
loadStateAdapter = ConversationLoadStateAdapter(adapter::retry)
|
|
||||||
|
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
setupRecyclerView()
|
||||||
layoutManager = LinearLayoutManager(view.context)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter)
|
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
||||||
|
|
||||||
binding.progressBar.hide()
|
|
||||||
binding.statusView.hide()
|
|
||||||
|
|
||||||
initSwipeToRefresh()
|
initSwipeToRefresh()
|
||||||
|
|
||||||
|
adapter.addLoadStateListener { loadState ->
|
||||||
|
if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.statusView.hide()
|
||||||
|
binding.progressBar.hide()
|
||||||
|
|
||||||
|
if (adapter.itemCount == 0) {
|
||||||
|
when (loadState.refresh) {
|
||||||
|
is LoadState.NotLoading -> {
|
||||||
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
|
binding.statusView.show()
|
||||||
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LoadState.Error -> {
|
||||||
|
binding.statusView.show()
|
||||||
|
|
||||||
|
if ((loadState.refresh as LoadState.Error).error is IOException) {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null)
|
||||||
|
} else {
|
||||||
|
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LoadState.Loading -> {
|
||||||
|
binding.progressBar.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
if (positionStart == 0 && adapter.itemCount != itemCount) {
|
||||||
|
binding.recyclerView.post {
|
||||||
|
if (getView() != null) {
|
||||||
|
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
hideFab = preferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||||
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val composeButton = (activity as ActionButtonActivity).actionButton
|
||||||
|
if (composeButton != null) {
|
||||||
|
if (hideFab) {
|
||||||
|
if (dy > 0 && composeButton.isShown) {
|
||||||
|
composeButton.hide() // hides the button if we're scrolling down
|
||||||
|
} else if (dy < 0 && !composeButton.isShown) {
|
||||||
|
composeButton.show() // shows it if we are scrolling up
|
||||||
|
}
|
||||||
|
} else if (!composeButton.isShown) {
|
||||||
|
composeButton.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||||
adapter.submitData(pagingData)
|
adapter.submitData(pagingData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.addLoadStateListener { loadStates ->
|
lifecycleScope.launchWhenResumed {
|
||||||
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||||
loadStates.refresh.let { refreshState ->
|
while (!useAbsoluteTime) {
|
||||||
if (refreshState is LoadState.Error) {
|
adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED))
|
||||||
binding.statusView.show()
|
delay(1.toDuration(DurationUnit.MINUTES))
|
||||||
if (refreshState.error is IOException) {
|
|
||||||
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
|
||||||
adapter.refresh()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
|
||||||
adapter.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.statusView.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0)
|
|
||||||
|
|
||||||
if (refreshState is LoadState.NotLoading && !initialRefreshDone) {
|
|
||||||
// jump to top after the initial refresh finished
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
initialRefreshDone = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshState != LoadState.Loading) {
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventHub.events
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||||
|
.subscribe { event ->
|
||||||
|
if (event is PreferenceChangedEvent) {
|
||||||
|
onPreferenceChanged(event.preferenceKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
|
||||||
|
binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
|
||||||
|
binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSwipeToRefresh() {
|
private fun initSwipeToRefresh() {
|
||||||
|
@ -207,7 +266,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenReblog(position: Int) {
|
override fun onOpenReblog(position: Int) {
|
||||||
// there are no reblogs in search results
|
// there are no reblogs in conversations
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
|
@ -252,6 +311,19 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
|
adapter.peek(position)?.let { conversation ->
|
||||||
|
viewModel.voteInPoll(choices, conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReselect() {
|
||||||
|
if (isAdded) {
|
||||||
|
binding.recyclerView.layoutManager?.scrollToPosition(0)
|
||||||
|
binding.recyclerView.stopScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteConversation(conversation: ConversationViewData) {
|
private fun deleteConversation(conversation: ConversationViewData) {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||||
|
@ -262,20 +334,20 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jumpToTop() {
|
private fun onPreferenceChanged(key: String) {
|
||||||
if (isAdded) {
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
layoutManager?.scrollToPosition(0)
|
when (key) {
|
||||||
binding.recyclerView.stopScroll()
|
PrefKeys.FAB_HIDE -> {
|
||||||
}
|
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||||
}
|
}
|
||||||
|
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
|
||||||
override fun onReselect() {
|
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
|
||||||
jumpToTop()
|
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
||||||
}
|
if (enabled != oldMediaPreviewEnabled) {
|
||||||
|
adapter.mediaPreviewEnabled = enabled
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
adapter.peek(position)?.let { conversation ->
|
}
|
||||||
viewModel.voteInPoll(choices, conversation)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.LoadType
|
import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
|
import androidx.room.withTransaction
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
class ConversationsRemoteMediator(
|
class ConversationsRemoteMediator(
|
||||||
|
@ -14,38 +17,53 @@ class ConversationsRemoteMediator(
|
||||||
private val db: AppDatabase
|
private val db: AppDatabase
|
||||||
) : RemoteMediator<Int, ConversationEntity>() {
|
) : RemoteMediator<Int, ConversationEntity>() {
|
||||||
|
|
||||||
|
private var nextKey: String? = null
|
||||||
|
|
||||||
|
private var order: Int = 0
|
||||||
|
|
||||||
override suspend fun load(
|
override suspend fun load(
|
||||||
loadType: LoadType,
|
loadType: LoadType,
|
||||||
state: PagingState<Int, ConversationEntity>
|
state: PagingState<Int, ConversationEntity>
|
||||||
): MediatorResult {
|
): MediatorResult {
|
||||||
|
|
||||||
|
if (loadType == LoadType.PREPEND) {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
nextKey = null
|
||||||
|
order = 0
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val conversationsResult = when (loadType) {
|
val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize)
|
||||||
LoadType.REFRESH -> {
|
|
||||||
api.getConversations(limit = state.config.initialLoadSize)
|
val conversations = conversationsResponse.body()
|
||||||
}
|
if (!conversationsResponse.isSuccessful || conversations == null) {
|
||||||
LoadType.PREPEND -> {
|
return MediatorResult.Error(HttpException(conversationsResponse))
|
||||||
return MediatorResult.Success(endOfPaginationReached = true)
|
|
||||||
}
|
|
||||||
LoadType.APPEND -> {
|
|
||||||
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id
|
|
||||||
api.getConversations(maxId = maxId, limit = state.config.pageSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadType == LoadType.REFRESH) {
|
db.withTransaction {
|
||||||
db.conversationDao().deleteForAccount(accountId)
|
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
db.conversationDao().deleteForAccount(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val linkHeader = conversationsResponse.headers()["Link"]
|
||||||
|
val links = HttpHeaderLink.parse(linkHeader)
|
||||||
|
nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
|
||||||
|
|
||||||
|
db.conversationDao().insert(
|
||||||
|
conversations
|
||||||
|
.filterNot { it.lastStatus == null }
|
||||||
|
.map {
|
||||||
|
it.toEntity(accountId, order++)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
db.conversationDao().insert(
|
return MediatorResult.Success(endOfPaginationReached = nextKey == null)
|
||||||
conversationsResult
|
|
||||||
.filterNot { it.lastStatus == null }
|
|
||||||
.map { it.toEntity(accountId) }
|
|
||||||
)
|
|
||||||
return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty())
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return MediatorResult.Error(e)
|
return MediatorResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
/* Copyright 2021 Tusky Contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ConversationsRepository @Inject constructor(
|
|
||||||
val mastodonApi: MastodonApi,
|
|
||||||
val db: AppDatabase
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun deleteCacheForAccount(accountId: Long) {
|
|
||||||
Single.fromCallable {
|
|
||||||
db.conversationDao().deleteForAccount(accountId)
|
|
||||||
}.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,7 +26,7 @@ import androidx.paging.map
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -41,7 +41,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val conversationFlow = Pager(
|
val conversationFlow = Pager(
|
||||||
config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20),
|
config = PagingConfig(pageSize = 30),
|
||||||
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
||||||
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
package com.keylesspalace.tusky.components.instanceinfo
|
package com.keylesspalace.tusky.components.instanceinfo
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
|
import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.EmojisEntity
|
import com.keylesspalace.tusky.db.EmojisEntity
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
@ -33,6 +34,7 @@ import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
@ -228,26 +230,50 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||||
).fold(
|
).fold(
|
||||||
{ accessToken ->
|
{ accessToken ->
|
||||||
accountManager.addAccount(accessToken.accessToken, domain, OAUTH_SCOPES)
|
fetchAccountDetails(accessToken, domain, clientId, clientSecret)
|
||||||
|
|
||||||
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 ->
|
{ e ->
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
binding.domainTextInputLayout.error =
|
binding.domainTextInputLayout.error =
|
||||||
getString(R.string.error_retrieving_oauth_token)
|
getString(R.string.error_retrieving_oauth_token)
|
||||||
Log.e(
|
Log.e(TAG, getString(R.string.error_retrieving_oauth_token), e)
|
||||||
TAG,
|
|
||||||
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchAccountDetails(
|
||||||
|
accessToken: AccessToken,
|
||||||
|
domain: String,
|
||||||
|
clientId: String,
|
||||||
|
clientSecret: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
mastodonApi.accountVerifyCredentials(
|
||||||
|
domain = domain,
|
||||||
|
auth = "Bearer ${accessToken.accessToken}"
|
||||||
|
).fold({ newAccount ->
|
||||||
|
accountManager.addAccount(
|
||||||
|
accessToken = accessToken.accessToken,
|
||||||
|
domain = domain,
|
||||||
|
clientId = clientId,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
oauthScopes = OAUTH_SCOPES,
|
||||||
|
newAccount = newAccount
|
||||||
|
)
|
||||||
|
|
||||||
|
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_loading_account_details)
|
||||||
|
Log.e(TAG, getString(R.string.error_loading_account_details), e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun setLoading(loadingState: Boolean) {
|
private fun setLoading(loadingState: Boolean) {
|
||||||
if (loadingState) {
|
if (loadingState) {
|
||||||
binding.loginLoadingLayout.visibility = View.VISIBLE
|
binding.loginLoadingLayout.visibility = View.VISIBLE
|
||||||
|
|
|
@ -83,6 +83,10 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
val data = OauthLogin.parseData(intent)
|
val data = OauthLogin.parseData(intent)
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
|
@ -458,24 +458,6 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void deleteLegacyNotificationChannels(@NonNull Context context, @NonNull AccountManager accountManager) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
// used until Tusky 1.4
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_FOLLOW);
|
|
||||||
|
|
||||||
// used until Tusky 1.7
|
|
||||||
for(AccountEntity account: accountManager.getAllAccountsOrderedByActive()) {
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE+" "+account.getIdentifier());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) {
|
public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
|
import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
|
@ -49,7 +51,12 @@ private fun accountNeedsMigration(account: AccountEntity): Boolean =
|
||||||
fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean =
|
fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean =
|
||||||
accountManager.activeAccount?.let(::accountNeedsMigration) ?: false
|
accountManager.activeAccount?.let(::accountNeedsMigration) ?: false
|
||||||
|
|
||||||
fun showMigrationNoticeIfNecessary(context: Context, parent: View, accountManager: AccountManager) {
|
fun showMigrationNoticeIfNecessary(
|
||||||
|
context: Context,
|
||||||
|
parent: View,
|
||||||
|
anchorView: View?,
|
||||||
|
accountManager: AccountManager
|
||||||
|
) {
|
||||||
// No point showing anything if we cannot enable it
|
// No point showing anything if we cannot enable it
|
||||||
if (!isUnifiedPushAvailable(context)) return
|
if (!isUnifiedPushAvailable(context)) return
|
||||||
if (!anyAccountNeedsMigration(accountManager)) return
|
if (!anyAccountNeedsMigration(accountManager)) return
|
||||||
|
@ -57,10 +64,10 @@ fun showMigrationNoticeIfNecessary(context: Context, parent: View, accountManage
|
||||||
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return
|
if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return
|
||||||
|
|
||||||
Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE).apply {
|
Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE)
|
||||||
setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) }
|
.setAnchorView(anchorView)
|
||||||
show()
|
.setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) }
|
||||||
}
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) {
|
private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) {
|
||||||
|
@ -87,7 +94,7 @@ private suspend fun enableUnifiedPushNotificationsForAccount(context: Context, a
|
||||||
// Already registered, update the subscription to match notification settings
|
// Already registered, update the subscription to match notification settings
|
||||||
updateUnifiedPushSubscription(context, api, accountManager, account)
|
updateUnifiedPushSubscription(context, api, accountManager, account)
|
||||||
} else {
|
} else {
|
||||||
UnifiedPush.registerAppWithDialog(context, account.id.toString())
|
UnifiedPush.registerAppWithDialog(context, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +157,14 @@ private fun buildSubscriptionData(context: Context, account: AccountEntity): Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by UnifiedPush callback
|
// Called by UnifiedPush callback
|
||||||
suspend fun registerUnifiedPushEndpoint(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity, endpoint: String) {
|
suspend fun registerUnifiedPushEndpoint(
|
||||||
|
context: Context,
|
||||||
|
api: MastodonApi,
|
||||||
|
accountManager: AccountManager,
|
||||||
|
account: AccountEntity,
|
||||||
|
endpoint: String
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
// Generate a prime256v1 key pair for WebPush
|
// Generate a prime256v1 key pair for WebPush
|
||||||
// Decryption is unimplemented for now, since Mastodon uses an old WebPush
|
// Decryption is unimplemented for now, since Mastodon uses an old WebPush
|
||||||
// standard which does not send needed information for decryption in the payload
|
// standard which does not send needed information for decryption in the payload
|
||||||
|
@ -159,27 +173,22 @@ suspend fun registerUnifiedPushEndpoint(context: Context, api: MastodonApi, acco
|
||||||
val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
|
val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
|
||||||
val auth = CryptoUtil.secureRandomBytesEncoded(16)
|
val auth = CryptoUtil.secureRandomBytesEncoded(16)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
api.subscribePushNotifications(
|
||||||
api.subscribePushNotifications(
|
"Bearer ${account.accessToken}", account.domain,
|
||||||
"Bearer ${account.accessToken}", account.domain,
|
endpoint, keyPair.pubkey, auth,
|
||||||
endpoint, keyPair.pubkey, auth,
|
buildSubscriptionData(context, account)
|
||||||
buildSubscriptionData(context, account)
|
).onFailure { throwable ->
|
||||||
).onFailure {
|
Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable)
|
||||||
Log.d(TAG, "Error setting push endpoint for account ${account.id}")
|
disableUnifiedPushNotificationsForAccount(context, account)
|
||||||
Log.d(TAG, Log.getStackTraceString(it))
|
}.onSuccess {
|
||||||
Log.d(TAG, (it as HttpException).response().toString())
|
Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}")
|
||||||
|
|
||||||
disableUnifiedPushNotificationsForAccount(context, account)
|
account.pushPubKey = keyPair.pubkey
|
||||||
}.onSuccess {
|
account.pushPrivKey = keyPair.privKey
|
||||||
Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}")
|
account.pushAuth = auth
|
||||||
|
account.pushServerKey = it.serverKey
|
||||||
account.pushPubKey = keyPair.pubkey
|
account.unifiedPushUrl = endpoint
|
||||||
account.pushPrivKey = keyPair.privKey
|
accountManager.saveAccount(account)
|
||||||
account.pushAuth = auth
|
|
||||||
account.pushServerKey = it.serverKey
|
|
||||||
account.unifiedPushUrl = endpoint
|
|
||||||
accountManager.saveAccount(account)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
checkBoxPreference {
|
checkBoxPreference {
|
||||||
setTitle(R.string.pref_title_show_replies)
|
setTitle(R.string.pref_title_show_replies)
|
||||||
key = PrefKeys.TAB_FILTER_HOME_REPLIES
|
key = PrefKeys.TAB_FILTER_HOME_REPLIES
|
||||||
setDefaultValue(false)
|
setDefaultValue(true)
|
||||||
isIconSpaceReserved = false
|
isIconSpaceReserved = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
|
|
@ -30,7 +30,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.NotestockApi
|
import com.keylesspalace.tusky.network.NotestockApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
|
@ -107,9 +107,6 @@ class TimelineFragment :
|
||||||
private lateinit var adapter: TimelinePagingAdapter
|
private lateinit var adapter: TimelinePagingAdapter
|
||||||
|
|
||||||
private var isSwipeToRefreshEnabled = true
|
private var isSwipeToRefreshEnabled = true
|
||||||
|
|
||||||
private var layoutManager: LinearLayoutManager? = null
|
|
||||||
private var scrollListener: RecyclerView.OnScrollListener? = null
|
|
||||||
private var hideFab = false
|
private var hideFab = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -244,7 +241,7 @@ class TimelineFragment :
|
||||||
if (actionButtonPresent()) {
|
if (actionButtonPresent()) {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
hideFab = preferences.getBoolean("fabHide", false)
|
hideFab = preferences.getBoolean("fabHide", false)
|
||||||
scrollListener = object : RecyclerView.OnScrollListener() {
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
val composeButton = (activity as ActionButtonActivity).actionButton
|
val composeButton = (activity as ActionButtonActivity).actionButton
|
||||||
if (composeButton != null) {
|
if (composeButton != null) {
|
||||||
|
@ -259,9 +256,7 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
})
|
||||||
binding.recyclerView.addOnScrollListener(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventHub.events
|
eventHub.events
|
||||||
|
@ -297,8 +292,7 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
layoutManager = LinearLayoutManager(context)
|
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
||||||
binding.recyclerView.addItemDecoration(divider)
|
binding.recyclerView.addItemDecoration(divider)
|
||||||
|
|
||||||
|
@ -509,7 +503,7 @@ class TimelineFragment :
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||||
if (!useAbsoluteTime) {
|
if (!useAbsoluteTime) {
|
||||||
Observable.interval(1, TimeUnit.MINUTES)
|
Observable.interval(0, 1, TimeUnit.MINUTES)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
|
@ -520,7 +514,7 @@ class TimelineFragment :
|
||||||
|
|
||||||
override fun onReselect() {
|
override fun onReselect() {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
layoutManager!!.scrollToPosition(0)
|
binding.recyclerView.layoutManager?.scrollToPosition(0)
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
contentShowing = false,
|
contentShowing = false,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
card = null,
|
card = null,
|
||||||
|
repliesCount = 0,
|
||||||
quote = null,
|
quote = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -141,6 +142,7 @@ fun Status.toEntity(
|
||||||
contentCollapsed = contentCollapsed,
|
contentCollapsed = contentCollapsed,
|
||||||
pinned = actionableStatus.pinned == true,
|
pinned = actionableStatus.pinned == true,
|
||||||
card = actionableStatus.card?.let(gson::toJson),
|
card = actionableStatus.card?.let(gson::toJson),
|
||||||
|
repliesCount = actionableStatus.repliesCount,
|
||||||
quote = actionableStatus.quote?.let(gson::toJson),
|
quote = actionableStatus.quote?.let(gson::toJson),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -186,6 +188,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
quote = quote,
|
quote = quote,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -216,6 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = null,
|
poll = null,
|
||||||
card = null,
|
card = null,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
quote = null,
|
quote = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -245,6 +249,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = card,
|
card = card,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
quote = quote,
|
quote = quote,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,10 @@ class CachedTimelineRemoteMediator(
|
||||||
state: PagingState<Int, TimelineStatusWithAccount>
|
state: PagingState<Int, TimelineStatusWithAccount>
|
||||||
): MediatorResult {
|
): MediatorResult {
|
||||||
|
|
||||||
|
if (!activeAccount.isLoggedIn()) {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var dbEmpty = false
|
var dbEmpty = false
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.ExperimentalPagingApi
|
import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.filter
|
import androidx.paging.filter
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
|
@ -37,11 +38,12 @@ import com.keylesspalace.tusky.components.timeline.toViewData
|
||||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
|
@ -69,7 +71,17 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val gson: Gson,
|
private val gson: Gson,
|
||||||
streamingManager: StreamingManager,
|
streamingManager: StreamingManager,
|
||||||
) : TimelineViewModel(timelineCases, api, eventHub, accountManager, sharedPreferences, filterModel, streamingManager) {
|
) : TimelineViewModel(
|
||||||
|
timelineCases,
|
||||||
|
api,
|
||||||
|
eventHub,
|
||||||
|
accountManager,
|
||||||
|
sharedPreferences,
|
||||||
|
filterModel,
|
||||||
|
streamingManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var currentPagingSource: PagingSource<Int, TimelineStatusWithAccount>? = null
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
override val statuses = Pager(
|
override val statuses = Pager(
|
||||||
|
@ -81,6 +93,8 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
EmptyTimelinePagingSource()
|
EmptyTimelinePagingSource()
|
||||||
} else {
|
} else {
|
||||||
db.timelineDao().getStatuses(activeAccount.id)
|
db.timelineDao().getStatuses(activeAccount.id)
|
||||||
|
}.also { newPagingSource ->
|
||||||
|
this.currentPagingSource = newPagingSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).flow
|
).flow
|
||||||
|
@ -116,13 +130,15 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
db.timelineDao().setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing)
|
db.timelineDao()
|
||||||
|
.setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
db.timelineDao().setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed)
|
db.timelineDao()
|
||||||
|
.setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,12 +165,21 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
timelineDao.insertStatus(
|
||||||
|
Placeholder(placeholderId, loading = true).toEntity(
|
||||||
|
activeAccount.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val response = db.withTransaction {
|
val response = db.withTransaction {
|
||||||
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
||||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
val nextPlaceholderId =
|
||||||
api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE)
|
timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||||
|
api.homeTimeline(
|
||||||
|
maxId = idAbovePlaceholder,
|
||||||
|
sinceId = nextPlaceholderId,
|
||||||
|
limit = LOAD_AT_ONCE
|
||||||
|
)
|
||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
val statuses = response.body()
|
val statuses = response.body()
|
||||||
|
@ -168,16 +193,21 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
timelineDao.delete(activeAccount.id, placeholderId)
|
timelineDao.delete(activeAccount.id, placeholderId)
|
||||||
|
|
||||||
val overlappedStatuses = if (statuses.isNotEmpty()) {
|
val overlappedStatuses = if (statuses.isNotEmpty()) {
|
||||||
timelineDao.deleteRange(activeAccount.id, statuses.last().id, statuses.first().id)
|
timelineDao.deleteRange(
|
||||||
|
activeAccount.id,
|
||||||
|
statuses.last().id,
|
||||||
|
statuses.first().id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
for (status in statuses) {
|
for (status in statuses) {
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
||||||
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
status.reblog?.account?.toEntity(activeAccount.id, gson)
|
||||||
timelineDao.insertAccount(rebloggedAccount)
|
?.let { rebloggedAccount ->
|
||||||
}
|
timelineDao.insertAccount(rebloggedAccount)
|
||||||
|
}
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
status.toEntity(
|
status.toEntity(
|
||||||
timelineUserId = activeAccount.id,
|
timelineUserId = activeAccount.id,
|
||||||
|
@ -196,7 +226,10 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
to guarantee the placeholder has an id that exists on the server as not all
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
servers handle client generated ids as expected */
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
Placeholder(
|
||||||
|
statuses.last().id,
|
||||||
|
loading = false
|
||||||
|
).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +244,8 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
private suspend fun loadMoreFailed(placeholderId: String, e: Exception) {
|
private suspend fun loadMoreFailed(placeholderId: String, e: Exception) {
|
||||||
Log.w("CachedTimelineVM", "failed loading statuses", e)
|
Log.w("CachedTimelineVM", "failed loading statuses", e)
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
db.timelineDao().insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
db.timelineDao()
|
||||||
|
.insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
override fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
||||||
|
@ -266,6 +300,13 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun invalidate() {
|
||||||
|
// invalidating when we don't have statuses yet can cause empty timelines because it cancels the network load
|
||||||
|
if (db.timelineDao().getStatusCount(accountManager.activeAccount!!.id) > 0) {
|
||||||
|
currentPagingSource?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_STATUSES_IN_CACHE = 1000
|
private const val MAX_STATUSES_IN_CACHE = 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.isLessThan
|
import com.keylesspalace.tusky.util.isLessThan
|
||||||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||||
|
@ -270,6 +270,10 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun invalidate() {
|
||||||
|
currentSource?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, HttpException::class)
|
@Throws(IOException::class, HttpException::class)
|
||||||
suspend fun fetchStatusesForKind(
|
suspend fun fetchStatusesForKind(
|
||||||
fromId: String?,
|
fromId: String?,
|
||||||
|
|
|
@ -41,8 +41,8 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -140,6 +140,7 @@ abstract class TimelineViewModel(
|
||||||
this.isStreamingEnabled = isStreamingEnabled
|
this.isStreamingEnabled = isStreamingEnabled
|
||||||
|
|
||||||
if (kind == Kind.HOME) {
|
if (kind == Kind.HOME) {
|
||||||
|
// Note the variable is "true if filter" but the underlying preference/settings text is "true if show"
|
||||||
filterRemoveReplies =
|
filterRemoveReplies =
|
||||||
!sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true)
|
!sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true)
|
||||||
filterRemoveReblogs =
|
filterRemoveReblogs =
|
||||||
|
@ -233,6 +234,9 @@ abstract class TimelineViewModel(
|
||||||
|
|
||||||
abstract fun fullReload()
|
abstract fun fullReload()
|
||||||
|
|
||||||
|
/** Triggered when currently displayed data must be reloaded. */
|
||||||
|
protected abstract suspend fun invalidate()
|
||||||
|
|
||||||
protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean {
|
protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean {
|
||||||
val status = statusViewData.asStatusOrNull()?.status ?: return false
|
val status = statusViewData.asStatusOrNull()?.status ?: return false
|
||||||
return status.inReplyToId != null && filterRemoveReplies ||
|
return status.inReplyToId != null && filterRemoveReplies ||
|
||||||
|
@ -353,6 +357,9 @@ abstract class TimelineViewModel(
|
||||||
filterContextMatchesKind(kind, it.context)
|
filterContextMatchesKind(kind, it.context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
// After the filters are loaded we need to reload displayed content to apply them.
|
||||||
|
// It can happen during the usage or at startup, when we get statuses before filters.
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,8 @@ data class AccountEntity(
|
||||||
@field:PrimaryKey(autoGenerate = true) var id: Long,
|
@field:PrimaryKey(autoGenerate = true) var id: Long,
|
||||||
val domain: String,
|
val domain: String,
|
||||||
var accessToken: String,
|
var accessToken: String,
|
||||||
|
var clientId: String?, // nullable for backward compatibility
|
||||||
|
var clientSecret: String?, // nullable for backward compatibility
|
||||||
var isActive: Boolean,
|
var isActive: Boolean,
|
||||||
var accountId: String = "",
|
var accountId: String = "",
|
||||||
var username: String = "",
|
var username: String = "",
|
||||||
|
@ -81,6 +83,15 @@ data class AccountEntity(
|
||||||
val fullName: String
|
val fullName: String
|
||||||
get() = "@$username@$domain"
|
get() = "@$username@$domain"
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
// deleting credentials so they cannot be used again
|
||||||
|
accessToken = ""
|
||||||
|
clientId = null
|
||||||
|
clientSecret = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLoggedIn() = accessToken.isNotEmpty()
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
|
@ -48,13 +48,22 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new empty account and makes it the active account.
|
* Adds a new account and makes it the active account.
|
||||||
* More account information has to be added later with [updateActiveAccount]
|
|
||||||
* or the account wont be saved to the database.
|
|
||||||
* @param accessToken the access token for the new account
|
* @param accessToken the access token for the new account
|
||||||
* @param domain the domain of the accounts Mastodon instance
|
* @param domain the domain of the accounts Mastodon instance
|
||||||
|
* @param clientId the oauth client id used to sign in the account
|
||||||
|
* @param clientSecret the oauth client secret used to sign in the account
|
||||||
|
* @param oauthScopes the oauth scopes granted to the account
|
||||||
|
* @param newAccount the [Account] as returned by the Mastodon Api
|
||||||
*/
|
*/
|
||||||
fun addAccount(accessToken: String, domain: String, oauthScopes: String) {
|
fun addAccount(
|
||||||
|
accessToken: String,
|
||||||
|
domain: String,
|
||||||
|
clientId: String,
|
||||||
|
clientSecret: String,
|
||||||
|
oauthScopes: String,
|
||||||
|
newAccount: Account
|
||||||
|
) {
|
||||||
|
|
||||||
activeAccount?.let {
|
activeAccount?.let {
|
||||||
it.isActive = false
|
it.isActive = false
|
||||||
|
@ -62,13 +71,35 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
|
|
||||||
accountDao.insertOrReplace(it)
|
accountDao.insertOrReplace(it)
|
||||||
}
|
}
|
||||||
|
// check if this is a relogin with an existing account, if yes update it, otherwise create a new one
|
||||||
|
val existingAccountIndex = accounts.indexOfFirst { account ->
|
||||||
|
domain == account.domain && newAccount.id == account.accountId
|
||||||
|
}
|
||||||
|
val newAccountEntity = if (existingAccountIndex != -1) {
|
||||||
|
accounts[existingAccountIndex].copy(
|
||||||
|
accessToken = accessToken,
|
||||||
|
clientId = clientId,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
oauthScopes = oauthScopes,
|
||||||
|
isActive = true
|
||||||
|
).also { accounts[existingAccountIndex] = it }
|
||||||
|
} else {
|
||||||
|
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||||
|
val newAccountId = maxAccountId + 1
|
||||||
|
AccountEntity(
|
||||||
|
id = newAccountId,
|
||||||
|
domain = domain.lowercase(Locale.ROOT),
|
||||||
|
accessToken = accessToken,
|
||||||
|
clientId = clientId,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
oauthScopes = oauthScopes,
|
||||||
|
isActive = true,
|
||||||
|
accountId = newAccount.id
|
||||||
|
).also { accounts.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
activeAccount = newAccountEntity
|
||||||
val newAccountId = maxAccountId + 1
|
updateActiveAccount(newAccount)
|
||||||
activeAccount = AccountEntity(
|
|
||||||
id = newAccountId, domain = domain.lowercase(Locale.ROOT),
|
|
||||||
accessToken = accessToken, oauthScopes = oauthScopes, isActive = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,11 +120,12 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
*/
|
*/
|
||||||
fun logActiveAccountOut(): AccountEntity? {
|
fun logActiveAccountOut(): AccountEntity? {
|
||||||
|
|
||||||
if (activeAccount == null) {
|
return activeAccount?.let { account ->
|
||||||
return null
|
|
||||||
} else {
|
account.logout()
|
||||||
accounts.remove(activeAccount!!)
|
|
||||||
accountDao.delete(activeAccount!!)
|
accounts.remove(account)
|
||||||
|
accountDao.delete(account)
|
||||||
|
|
||||||
if (accounts.size > 0) {
|
if (accounts.size > 0) {
|
||||||
accounts[0].isActive = true
|
accounts[0].isActive = true
|
||||||
|
@ -103,7 +135,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
} else {
|
} else {
|
||||||
activeAccount = null
|
activeAccount = null
|
||||||
}
|
}
|
||||||
return activeAccount
|
activeAccount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,17 +155,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
it.emojis = account.emojis ?: emptyList()
|
it.emojis = account.emojis ?: emptyList()
|
||||||
|
|
||||||
Log.d(TAG, "updateActiveAccount: saving account with id " + it.id)
|
Log.d(TAG, "updateActiveAccount: saving account with id " + it.id)
|
||||||
it.id = accountDao.insertOrReplace(it)
|
accountDao.insertOrReplace(it)
|
||||||
|
|
||||||
val accountIndex = accounts.indexOf(it)
|
|
||||||
|
|
||||||
if (accountIndex != -1) {
|
|
||||||
// in case the user was already logged in with this account, remove the old information
|
|
||||||
accounts.removeAt(accountIndex)
|
|
||||||
accounts.add(accountIndex, it)
|
|
||||||
} else {
|
|
||||||
accounts.add(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 36)
|
}, version = 39)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -554,4 +554,32 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''");
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_36_37 = new Migration(36, 37) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `repliesCount` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_repliesCount` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_37_38 = new Migration(37, 38) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
// database needs to be cleaned because the ConversationAccountEntity got a new attribute
|
||||||
|
database.execSQL("DELETE FROM `ConversationEntity`");
|
||||||
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `order` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
|
||||||
|
// timestamps are now serialized differently so all cache tables that contain them need to be cleaned
|
||||||
|
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_38_39 = new Migration(38, 39) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientId` TEXT");
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,14 @@ interface ConversationsDao {
|
||||||
suspend fun insert(conversations: List<ConversationEntity>)
|
suspend fun insert(conversations: List<ConversationEntity>)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(conversation: ConversationEntity): Long
|
suspend fun insert(conversation: ConversationEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId")
|
@Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId")
|
||||||
suspend fun delete(id: String, accountId: Long): Int
|
suspend fun delete(id: String, accountId: Long)
|
||||||
|
|
||||||
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY `order` ASC")
|
||||||
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
||||||
|
|
||||||
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
|
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
|
||||||
fun deleteForAccount(accountId: Long)
|
suspend fun deleteForAccount(accountId: Long)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface InstanceDao {
|
interface InstanceDao {
|
||||||
|
@ -29,9 +30,11 @@ interface InstanceDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
||||||
suspend fun insertOrReplace(emojis: EmojisEntity)
|
suspend fun insertOrReplace(emojis: EmojisEntity)
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
suspend fun getInstanceInfo(instance: String): InstanceInfoEntity?
|
suspend fun getInstanceInfo(instance: String): InstanceInfoEntity?
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
suspend fun getEmojiInfo(instance: String): EmojisEntity?
|
suspend fun getEmojiInfo(instance: String): EmojisEntity?
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ abstract class TimelineDao {
|
||||||
"""
|
"""
|
||||||
SELECT s.serverId, s.url, s.timelineUserId,
|
SELECT s.serverId, s.url, s.timelineUserId,
|
||||||
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
||||||
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
||||||
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
||||||
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
||||||
s.quote,
|
s.quote,
|
||||||
|
@ -198,4 +198,7 @@ AND timelineUserId = :accountId
|
||||||
*/
|
*/
|
||||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||||
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
|
||||||
|
abstract suspend fun getStatusCount(accountId: Long): Int
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ data class TimelineStatusEntity(
|
||||||
val emojis: String?,
|
val emojis: String?,
|
||||||
val reblogsCount: Int,
|
val reblogsCount: Int,
|
||||||
val favouritesCount: Int,
|
val favouritesCount: Int,
|
||||||
|
val repliesCount: Int,
|
||||||
val reblogged: Boolean,
|
val reblogged: Boolean,
|
||||||
val bookmarked: Boolean,
|
val bookmarked: Boolean,
|
||||||
val favourited: Boolean,
|
val favourited: Boolean,
|
||||||
|
|
|
@ -70,7 +70,8 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||||
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||||
AppDatabase.MIGRATION_35_36,
|
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
|
||||||
|
AppDatabase.MIGRATION_38_39
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,12 @@ package com.keylesspalace.tusky.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter
|
||||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
|
@ -40,6 +42,7 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@ -52,7 +55,9 @@ class NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson() = Gson()
|
fun providesGson(): Gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
|
.create()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -109,7 +114,7 @@ class NetworkModule {
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||||
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ data class Status(
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||||
@SerializedName("favourites_count") val favouritesCount: Int,
|
@SerializedName("favourites_count") val favouritesCount: Int,
|
||||||
|
@SerializedName("replies_count") val repliesCount: Int,
|
||||||
var reblogged: Boolean,
|
var reblogged: Boolean,
|
||||||
var favourited: Boolean,
|
var favourited: Boolean,
|
||||||
var bookmarked: Boolean,
|
var bookmarked: Boolean,
|
||||||
|
|
|
@ -547,7 +547,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
||||||
updateViewDataAt(position, (vd) -> vd.copyWIthCollapsed(isCollapsed));
|
updateViewDataAt(position, (vd) -> vd.copyWithCollapsed(isCollapsed));
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -972,10 +972,10 @@ public class NotificationsFragment extends SFragment implements
|
||||||
if (notifications.size() == 0 && adapter.getItemCount() == 0) {
|
if (notifications.size() == 0 && adapter.getItemCount() == 0) {
|
||||||
this.statusView.setVisibility(View.VISIBLE);
|
this.statusView.setVisibility(View.VISIBLE);
|
||||||
this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
|
this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
|
||||||
} else {
|
|
||||||
swipeRefreshLayout.setEnabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFilterVisibility();
|
updateFilterVisibility();
|
||||||
|
swipeRefreshLayout.setEnabled(true);
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
progressBar.setVisibility(View.GONE);
|
progressBar.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
@ -1240,7 +1240,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||||
if (!useAbsoluteTime) {
|
if (!useAbsoluteTime) {
|
||||||
Observable.interval(1, TimeUnit.MINUTES)
|
Observable.interval(0, 1, TimeUnit.MINUTES)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.to(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE)))
|
.to(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE)))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
|
@ -56,7 +56,7 @@ import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.usecase.TimelineCases;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.StatusParsingHelper;
|
import com.keylesspalace.tusky.util.StatusParsingHelper;
|
||||||
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
||||||
|
|
|
@ -397,7 +397,7 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
||||||
adapter.setItem(
|
adapter.setItem(
|
||||||
position,
|
position,
|
||||||
statuses.getPairedItem(position).copyWIthCollapsed(isCollapsed),
|
statuses.getPairedItem(position).copyWithCollapsed(isCollapsed),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 FasterXML, LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||||
|
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||||
|
*
|
||||||
|
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
|
||||||
|
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
|
||||||
|
* objects.
|
||||||
|
*
|
||||||
|
* Supported parse format:
|
||||||
|
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]`
|
||||||
|
*
|
||||||
|
* @see [this specification](http://www.w3.org/TR/NOTE-datetime)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** ID to represent the 'GMT' string */
|
||||||
|
private const val GMT_ID = "GMT"
|
||||||
|
|
||||||
|
/** The GMT timezone, prefetched to avoid more lookups. */
|
||||||
|
private val TIMEZONE_Z: TimeZone = TimeZone.getTimeZone(GMT_ID)
|
||||||
|
|
||||||
|
/** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */
|
||||||
|
internal fun Date.formatIsoDate(): String {
|
||||||
|
val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US)
|
||||||
|
calendar.time = this
|
||||||
|
|
||||||
|
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
|
||||||
|
val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length
|
||||||
|
val formatted = StringBuilder(capacity)
|
||||||
|
padInt(formatted, calendar[Calendar.YEAR], "yyyy".length)
|
||||||
|
formatted.append('-')
|
||||||
|
padInt(formatted, calendar[Calendar.MONTH] + 1, "MM".length)
|
||||||
|
formatted.append('-')
|
||||||
|
padInt(formatted, calendar[Calendar.DAY_OF_MONTH], "dd".length)
|
||||||
|
formatted.append('T')
|
||||||
|
padInt(formatted, calendar[Calendar.HOUR_OF_DAY], "hh".length)
|
||||||
|
formatted.append(':')
|
||||||
|
padInt(formatted, calendar[Calendar.MINUTE], "mm".length)
|
||||||
|
formatted.append(':')
|
||||||
|
padInt(formatted, calendar[Calendar.SECOND], "ss".length)
|
||||||
|
formatted.append('.')
|
||||||
|
padInt(formatted, calendar[Calendar.MILLISECOND], "sss".length)
|
||||||
|
formatted.append('Z')
|
||||||
|
return formatted.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date from ISO-8601 formatted string. It expects a format
|
||||||
|
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]`
|
||||||
|
*
|
||||||
|
* @receiver ISO string to parse in the appropriate format.
|
||||||
|
* @return the parsed date
|
||||||
|
*/
|
||||||
|
internal fun String.parseIsoDate(): Date {
|
||||||
|
return try {
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
// extract year
|
||||||
|
val year = parseInt(this, offset, 4.let { offset += it; offset })
|
||||||
|
if (checkOffset(this, offset, '-')) {
|
||||||
|
offset += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract month
|
||||||
|
val month = parseInt(this, offset, 2.let { offset += it; offset })
|
||||||
|
if (checkOffset(this, offset, '-')) {
|
||||||
|
offset += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract day
|
||||||
|
val day = parseInt(this, offset, 2.let { offset += it; offset })
|
||||||
|
// default time value
|
||||||
|
var hour = 0
|
||||||
|
var minutes = 0
|
||||||
|
var seconds = 0
|
||||||
|
// always use 0 otherwise returned date will include millis of current time
|
||||||
|
var milliseconds = 0
|
||||||
|
|
||||||
|
// if the value has no time component (and no time zone), we are done
|
||||||
|
val hasT = checkOffset(this, offset, 'T')
|
||||||
|
if (!hasT && this.length <= offset) {
|
||||||
|
return GregorianCalendar(year, month - 1, day).time
|
||||||
|
}
|
||||||
|
if (hasT) {
|
||||||
|
|
||||||
|
// extract hours, minutes, seconds and milliseconds
|
||||||
|
hour = parseInt(this, 1.let { offset += it; offset }, 2.let { offset += it; offset })
|
||||||
|
if (checkOffset(this, offset, ':')) {
|
||||||
|
offset += 1
|
||||||
|
}
|
||||||
|
minutes = parseInt(this, offset, 2.let { offset += it; offset })
|
||||||
|
if (checkOffset(this, offset, ':')) {
|
||||||
|
offset += 1
|
||||||
|
}
|
||||||
|
// second and milliseconds can be optional
|
||||||
|
if (this.length > offset) {
|
||||||
|
val c = this[offset]
|
||||||
|
if (c != 'Z' && c != '+' && c != '-') {
|
||||||
|
seconds = parseInt(this, offset, 2.let { offset += it; offset })
|
||||||
|
if (seconds in 60..62) seconds = 59 // truncate up to 3 leap seconds
|
||||||
|
// milliseconds can be optional in the format
|
||||||
|
if (checkOffset(this, offset, '.')) {
|
||||||
|
offset += 1
|
||||||
|
val endOffset = indexOfNonDigit(this, offset + 1) // assume at least one digit
|
||||||
|
val parseEndOffset = min(endOffset, offset + 3) // parse up to 3 digits
|
||||||
|
val fraction = parseInt(this, offset, parseEndOffset)
|
||||||
|
milliseconds =
|
||||||
|
(10.0.pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt()
|
||||||
|
offset = endOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract timezone
|
||||||
|
require(this.length > offset) { "No time zone indicator" }
|
||||||
|
val timezone: TimeZone
|
||||||
|
val timezoneIndicator = this[offset]
|
||||||
|
if (timezoneIndicator == 'Z') {
|
||||||
|
timezone = TIMEZONE_Z
|
||||||
|
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
||||||
|
val timezoneOffset = this.substring(offset)
|
||||||
|
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
|
||||||
|
if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) {
|
||||||
|
timezone = TIMEZONE_Z
|
||||||
|
} else {
|
||||||
|
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
|
||||||
|
// not sure why, but it is what it is.
|
||||||
|
val timezoneId = GMT_ID + timezoneOffset
|
||||||
|
timezone = TimeZone.getTimeZone(timezoneId)
|
||||||
|
val act = timezone.id
|
||||||
|
if (act != timezoneId) {
|
||||||
|
/*
|
||||||
|
* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
|
||||||
|
* one without. If so, don't sweat.
|
||||||
|
* Yes, very inefficient. Hopefully not hit often.
|
||||||
|
* If it becomes a perf problem, add 'loose' comparison instead.
|
||||||
|
*/
|
||||||
|
val cleaned = act.replace(":", "")
|
||||||
|
if (cleaned != timezoneId) {
|
||||||
|
throw IndexOutOfBoundsException(
|
||||||
|
"Mismatching time zone indicator: $timezoneId given, resolves to ${timezone.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw IndexOutOfBoundsException(
|
||||||
|
"Invalid time zone indicator '$timezoneIndicator'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val calendar: Calendar = GregorianCalendar(timezone)
|
||||||
|
calendar.isLenient = false
|
||||||
|
calendar[Calendar.YEAR] = year
|
||||||
|
calendar[Calendar.MONTH] = month - 1
|
||||||
|
calendar[Calendar.DAY_OF_MONTH] = day
|
||||||
|
calendar[Calendar.HOUR_OF_DAY] = hour
|
||||||
|
calendar[Calendar.MINUTE] = minutes
|
||||||
|
calendar[Calendar.SECOND] = seconds
|
||||||
|
calendar[Calendar.MILLISECOND] = milliseconds
|
||||||
|
calendar.time
|
||||||
|
// If we get a ParseException it'll already have the right message/offset.
|
||||||
|
// Other exception types can convert here.
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
throw JsonParseException("Not an RFC 3339 date: $this", e)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw JsonParseException("Not an RFC 3339 date: $this", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the expected character exist at the given offset in the value.
|
||||||
|
*
|
||||||
|
* @param value the string to check at the specified offset
|
||||||
|
* @param offset the offset to look for the expected character
|
||||||
|
* @param expected the expected character
|
||||||
|
* @return true if the expected character exist at the given offset
|
||||||
|
*/
|
||||||
|
private fun checkOffset(value: String, offset: Int, expected: Char): Boolean {
|
||||||
|
return offset < value.length && value[offset] == expected
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer located between 2 given offsets in a string
|
||||||
|
*
|
||||||
|
* @param value the string to parse
|
||||||
|
* @param beginIndex the start index for the integer in the string
|
||||||
|
* @param endIndex the end index for the integer in the string
|
||||||
|
* @return the int
|
||||||
|
* @throws NumberFormatException if the value is not a number
|
||||||
|
*/
|
||||||
|
private fun parseInt(value: String, beginIndex: Int, endIndex: Int): Int {
|
||||||
|
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
|
||||||
|
throw NumberFormatException(value)
|
||||||
|
}
|
||||||
|
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
|
||||||
|
var i = beginIndex
|
||||||
|
var result = 0
|
||||||
|
var digit: Int
|
||||||
|
if (i < endIndex) {
|
||||||
|
digit = Character.digit(value[i++], 10)
|
||||||
|
if (digit < 0) {
|
||||||
|
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||||
|
}
|
||||||
|
result = -digit
|
||||||
|
}
|
||||||
|
while (i < endIndex) {
|
||||||
|
digit = Character.digit(value[i++], 10)
|
||||||
|
if (digit < 0) {
|
||||||
|
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||||
|
}
|
||||||
|
result *= 10
|
||||||
|
result -= digit
|
||||||
|
}
|
||||||
|
return -result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zero pad a number to a specified length
|
||||||
|
*
|
||||||
|
* @param buffer buffer to use for padding
|
||||||
|
* @param value the integer value to pad if necessary.
|
||||||
|
* @param length the length of the string we should zero pad
|
||||||
|
*/
|
||||||
|
private fun padInt(buffer: StringBuilder, value: Int, length: Int) {
|
||||||
|
val strValue = value.toString()
|
||||||
|
for (i in length - strValue.length downTo 1) {
|
||||||
|
buffer.append('0')
|
||||||
|
}
|
||||||
|
buffer.append(strValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the first character in the string that is not a digit, starting at offset.
|
||||||
|
*/
|
||||||
|
private fun indexOfNonDigit(string: String, offset: Int): Int {
|
||||||
|
for (i in offset until string.length) {
|
||||||
|
val c = string[i]
|
||||||
|
if (c < '0' || c > '9') return i
|
||||||
|
}
|
||||||
|
return string.length
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class Rfc3339DateJsonAdapter : TypeAdapter<Date?>() {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun write(writer: JsonWriter, date: Date?) {
|
||||||
|
if (date == null) {
|
||||||
|
writer.nullValue()
|
||||||
|
} else {
|
||||||
|
writer.value(date.formatIsoDate())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun read(reader: JsonReader): Date? {
|
||||||
|
return when (reader.peek()) {
|
||||||
|
JsonToken.NULL -> {
|
||||||
|
reader.nextNull()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
reader.nextString().parseIsoDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,76 +0,0 @@
|
||||||
/* Copyright 2018 charlag
|
|
||||||
*
|
|
||||||
* 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.network;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.Interceptor;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by charlag on 31/10/17.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public final class InstanceSwitchAuthInterceptor implements Interceptor {
|
|
||||||
private AccountManager accountManager;
|
|
||||||
|
|
||||||
public InstanceSwitchAuthInterceptor(AccountManager accountManager) {
|
|
||||||
this.accountManager = accountManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response intercept(@NonNull Chain chain) throws IOException {
|
|
||||||
|
|
||||||
Request originalRequest = chain.request();
|
|
||||||
|
|
||||||
// only switch domains if the request comes from retrofit
|
|
||||||
if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) {
|
|
||||||
AccountEntity currentAccount = accountManager.getActiveAccount();
|
|
||||||
|
|
||||||
Request.Builder builder = originalRequest.newBuilder();
|
|
||||||
|
|
||||||
String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER);
|
|
||||||
if (instanceHeader != null) {
|
|
||||||
// use domain explicitly specified in custom header
|
|
||||||
builder.url(swapHost(originalRequest.url(), instanceHeader));
|
|
||||||
builder.removeHeader(MastodonApi.DOMAIN_HEADER);
|
|
||||||
} else if (currentAccount != null) {
|
|
||||||
//use domain of current account
|
|
||||||
builder.url(swapHost(originalRequest.url(), currentAccount.getDomain()))
|
|
||||||
.header("Authorization",
|
|
||||||
String.format("Bearer %s", currentAccount.getAccessToken()));
|
|
||||||
}
|
|
||||||
Request newRequest = builder.build();
|
|
||||||
|
|
||||||
return chain.proceed(newRequest);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return chain.proceed(originalRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) {
|
|
||||||
return url.newBuilder().host(host).build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/* 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.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class InstanceSwitchAuthInterceptor(private val accountManager: AccountManager) : Interceptor {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest: Request = chain.request()
|
||||||
|
|
||||||
|
// only switch domains if the request comes from retrofit
|
||||||
|
return if (originalRequest.url.host == MastodonApi.PLACEHOLDER_DOMAIN) {
|
||||||
|
|
||||||
|
val builder: Request.Builder = originalRequest.newBuilder()
|
||||||
|
val instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER)
|
||||||
|
|
||||||
|
if (instanceHeader != null) {
|
||||||
|
// use domain explicitly specified in custom header
|
||||||
|
builder.url(swapHost(originalRequest.url, instanceHeader))
|
||||||
|
builder.removeHeader(MastodonApi.DOMAIN_HEADER)
|
||||||
|
} else {
|
||||||
|
val currentAccount = accountManager.activeAccount
|
||||||
|
|
||||||
|
if (currentAccount != null) {
|
||||||
|
val accessToken = currentAccount.accessToken
|
||||||
|
if (accessToken.isNotEmpty()) {
|
||||||
|
// use domain of current account
|
||||||
|
builder.url(swapHost(originalRequest.url, currentAccount.domain))
|
||||||
|
.header("Authorization", "Bearer %s".format(accessToken))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newRequest: Request = builder.build()
|
||||||
|
|
||||||
|
if (MastodonApi.PLACEHOLDER_DOMAIN == newRequest.url.host) {
|
||||||
|
Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url)
|
||||||
|
return Response.Builder()
|
||||||
|
.code(400)
|
||||||
|
.message("Bad Request")
|
||||||
|
.protocol(Protocol.HTTP_2)
|
||||||
|
.body("".toResponseBody("text/plain".toMediaType()))
|
||||||
|
.request(chain.request())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.proceed(newRequest)
|
||||||
|
} else {
|
||||||
|
chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun swapHost(url: HttpUrl, host: String): HttpUrl {
|
||||||
|
return url.newBuilder().host(host).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import com.keylesspalace.tusky.entity.AccessToken
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Announcement
|
import com.keylesspalace.tusky.entity.Announcement
|
||||||
|
@ -74,10 +75,10 @@ interface MastodonApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET("/api/v1/custom_emojis")
|
@GET("/api/v1/custom_emojis")
|
||||||
suspend fun getCustomEmojis(): Result<List<Emoji>>
|
suspend fun getCustomEmojis(): NetworkResult<List<Emoji>>
|
||||||
|
|
||||||
@GET("api/v1/instance")
|
@GET("api/v1/instance")
|
||||||
suspend fun getInstance(): Result<Instance>
|
suspend fun getInstance(): NetworkResult<Instance>
|
||||||
|
|
||||||
@GET("api/v1/filters")
|
@GET("api/v1/filters")
|
||||||
fun getFilters(): Single<List<Filter>>
|
fun getFilters(): Single<List<Filter>>
|
||||||
|
@ -145,7 +146,7 @@ interface MastodonApi {
|
||||||
suspend fun updateMedia(
|
suspend fun updateMedia(
|
||||||
@Path("mediaId") mediaId: String,
|
@Path("mediaId") mediaId: String,
|
||||||
@Field("description") description: String
|
@Field("description") description: String
|
||||||
): Result<Attachment>
|
): NetworkResult<Attachment>
|
||||||
|
|
||||||
@GET("api/v1/media/{mediaId}")
|
@GET("api/v1/media/{mediaId}")
|
||||||
suspend fun getMedia(
|
suspend fun getMedia(
|
||||||
|
@ -158,7 +159,7 @@ interface MastodonApi {
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Header("Idempotency-Key") idempotencyKey: String,
|
@Header("Idempotency-Key") idempotencyKey: String,
|
||||||
@Body status: NewStatus
|
@Body status: NewStatus
|
||||||
): Result<Status>
|
): NetworkResult<Status>
|
||||||
|
|
||||||
@GET("api/v1/statuses/{id}")
|
@GET("api/v1/statuses/{id}")
|
||||||
fun status(
|
fun status(
|
||||||
|
@ -246,10 +247,13 @@ interface MastodonApi {
|
||||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||||
suspend fun deleteScheduledStatus(
|
suspend fun deleteScheduledStatus(
|
||||||
@Path("id") scheduledStatusId: String
|
@Path("id") scheduledStatusId: String
|
||||||
): Result<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
|
|
||||||
@GET("api/v1/accounts/verify_credentials")
|
@GET("api/v1/accounts/verify_credentials")
|
||||||
suspend fun accountVerifyCredentials(): Result<Account>
|
suspend fun accountVerifyCredentials(
|
||||||
|
@Header(DOMAIN_HEADER) domain: String? = null,
|
||||||
|
@Header("Authorization") auth: String? = null,
|
||||||
|
): NetworkResult<Account>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PATCH("api/v1/accounts/update_credentials")
|
@PATCH("api/v1/accounts/update_credentials")
|
||||||
|
@ -274,7 +278,7 @@ interface MastodonApi {
|
||||||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||||
): Result<Account>
|
): NetworkResult<Account>
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
suspend fun searchAccounts(
|
suspend fun searchAccounts(
|
||||||
|
@ -282,15 +286,15 @@ interface MastodonApi {
|
||||||
@Query("resolve") resolve: Boolean? = null,
|
@Query("resolve") resolve: Boolean? = null,
|
||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Result<List<TimelineAccount>>
|
): NetworkResult<List<TimelineAccount>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
fun searchAccountsCall(
|
fun searchAccountsSync(
|
||||||
@Query("q") query: String,
|
@Query("q") query: String,
|
||||||
@Query("resolve") resolve: Boolean? = null,
|
@Query("resolve") resolve: Boolean? = null,
|
||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Call<List<TimelineAccount>>
|
): NetworkResult<List<TimelineAccount>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}")
|
@GET("api/v1/accounts/{id}")
|
||||||
fun account(
|
fun account(
|
||||||
|
@ -445,7 +449,7 @@ interface MastodonApi {
|
||||||
@Field("redirect_uris") redirectUris: String,
|
@Field("redirect_uris") redirectUris: String,
|
||||||
@Field("scopes") scopes: String,
|
@Field("scopes") scopes: String,
|
||||||
@Field("website") website: String
|
@Field("website") website: String
|
||||||
): Result<AppCredentials>
|
): NetworkResult<AppCredentials>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
|
@ -456,34 +460,42 @@ interface MastodonApi {
|
||||||
@Field("redirect_uri") redirectUri: String,
|
@Field("redirect_uri") redirectUri: String,
|
||||||
@Field("code") code: String,
|
@Field("code") code: String,
|
||||||
@Field("grant_type") grantType: String
|
@Field("grant_type") grantType: String
|
||||||
): Result<AccessToken>
|
): NetworkResult<AccessToken>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("oauth/revoke")
|
||||||
|
suspend fun revokeOAuthToken(
|
||||||
|
@Field("client_id") clientId: String,
|
||||||
|
@Field("client_secret") clientSecret: String,
|
||||||
|
@Field("token") token: String
|
||||||
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@GET("/api/v1/lists")
|
@GET("/api/v1/lists")
|
||||||
suspend fun getLists(): Result<List<MastoList>>
|
suspend fun getLists(): NetworkResult<List<MastoList>>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/lists")
|
@POST("api/v1/lists")
|
||||||
suspend fun createList(
|
suspend fun createList(
|
||||||
@Field("title") title: String
|
@Field("title") title: String
|
||||||
): Result<MastoList>
|
): NetworkResult<MastoList>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("api/v1/lists/{listId}")
|
@PUT("api/v1/lists/{listId}")
|
||||||
suspend fun updateList(
|
suspend fun updateList(
|
||||||
@Path("listId") listId: String,
|
@Path("listId") listId: String,
|
||||||
@Field("title") title: String
|
@Field("title") title: String
|
||||||
): Result<MastoList>
|
): NetworkResult<MastoList>
|
||||||
|
|
||||||
@DELETE("api/v1/lists/{listId}")
|
@DELETE("api/v1/lists/{listId}")
|
||||||
suspend fun deleteList(
|
suspend fun deleteList(
|
||||||
@Path("listId") listId: String
|
@Path("listId") listId: String
|
||||||
): Result<Unit>
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@GET("api/v1/lists/{listId}/accounts")
|
@GET("api/v1/lists/{listId}/accounts")
|
||||||
suspend fun getAccountsInList(
|
suspend fun getAccountsInList(
|
||||||
@Path("listId") listId: String,
|
@Path("listId") listId: String,
|
||||||
@Query("limit") limit: Int
|
@Query("limit") limit: Int
|
||||||
): Result<List<TimelineAccount>>
|
): NetworkResult<List<TimelineAccount>>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
// @DELETE doesn't support fields
|
// @DELETE doesn't support fields
|
||||||
|
@ -491,20 +503,20 @@ interface MastodonApi {
|
||||||
suspend fun deleteAccountFromList(
|
suspend fun deleteAccountFromList(
|
||||||
@Path("listId") listId: String,
|
@Path("listId") listId: String,
|
||||||
@Field("account_ids[]") accountIds: List<String>
|
@Field("account_ids[]") accountIds: List<String>
|
||||||
): Result<Unit>
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/lists/{listId}/accounts")
|
@POST("api/v1/lists/{listId}/accounts")
|
||||||
suspend fun addAccountToList(
|
suspend fun addAccountToList(
|
||||||
@Path("listId") listId: String,
|
@Path("listId") listId: String,
|
||||||
@Field("account_ids[]") accountIds: List<String>
|
@Field("account_ids[]") accountIds: List<String>
|
||||||
): Result<Unit>
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@GET("/api/v1/conversations")
|
@GET("/api/v1/conversations")
|
||||||
suspend fun getConversations(
|
suspend fun getConversations(
|
||||||
@Query("max_id") maxId: String? = null,
|
@Query("max_id") maxId: String? = null,
|
||||||
@Query("limit") limit: Int
|
@Query("limit") limit: Int? = null
|
||||||
): List<Conversation>
|
): Response<List<Conversation>>
|
||||||
|
|
||||||
@DELETE("/api/v1/conversations/{id}")
|
@DELETE("/api/v1/conversations/{id}")
|
||||||
suspend fun deleteConversation(
|
suspend fun deleteConversation(
|
||||||
|
@ -547,24 +559,24 @@ interface MastodonApi {
|
||||||
@GET("api/v1/announcements")
|
@GET("api/v1/announcements")
|
||||||
suspend fun listAnnouncements(
|
suspend fun listAnnouncements(
|
||||||
@Query("with_dismissed") withDismissed: Boolean = true
|
@Query("with_dismissed") withDismissed: Boolean = true
|
||||||
): Result<List<Announcement>>
|
): NetworkResult<List<Announcement>>
|
||||||
|
|
||||||
@POST("api/v1/announcements/{id}/dismiss")
|
@POST("api/v1/announcements/{id}/dismiss")
|
||||||
suspend fun dismissAnnouncement(
|
suspend fun dismissAnnouncement(
|
||||||
@Path("id") announcementId: String
|
@Path("id") announcementId: String
|
||||||
): Result<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
|
|
||||||
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
||||||
suspend fun addAnnouncementReaction(
|
suspend fun addAnnouncementReaction(
|
||||||
@Path("id") announcementId: String,
|
@Path("id") announcementId: String,
|
||||||
@Path("name") name: String
|
@Path("name") name: String
|
||||||
): Result<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
|
|
||||||
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
||||||
suspend fun removeAnnouncementReaction(
|
suspend fun removeAnnouncementReaction(
|
||||||
@Path("id") announcementId: String,
|
@Path("id") announcementId: String,
|
||||||
@Path("name") name: String
|
@Path("name") name: String
|
||||||
): Result<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/reports")
|
@POST("api/v1/reports")
|
||||||
|
@ -601,14 +613,14 @@ interface MastodonApi {
|
||||||
): Single<SearchResult>
|
): Single<SearchResult>
|
||||||
|
|
||||||
@GET("api/v2/search")
|
@GET("api/v2/search")
|
||||||
fun searchCall(
|
fun searchSync(
|
||||||
@Query("q") query: String?,
|
@Query("q") query: String?,
|
||||||
@Query("type") type: String? = null,
|
@Query("type") type: String? = null,
|
||||||
@Query("resolve") resolve: Boolean? = null,
|
@Query("resolve") resolve: Boolean? = null,
|
||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
@Query("offset") offset: Int? = null,
|
@Query("offset") offset: Int? = null,
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Call<SearchResult>
|
): NetworkResult<SearchResult>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/accounts/{id}/note")
|
@POST("api/v1/accounts/{id}/note")
|
||||||
|
@ -629,7 +641,7 @@ interface MastodonApi {
|
||||||
// Should be generated dynamically from all the available notification
|
// Should be generated dynamically from all the available notification
|
||||||
// types defined in [com.keylesspalace.tusky.entities.Notification.Types]
|
// types defined in [com.keylesspalace.tusky.entities.Notification.Types]
|
||||||
@FieldMap data: Map<String, Boolean>
|
@FieldMap data: Map<String, Boolean>
|
||||||
): Result<NotificationSubscribeResult>
|
): NetworkResult<NotificationSubscribeResult>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("api/v1/push/subscription")
|
@PUT("api/v1/push/subscription")
|
||||||
|
@ -637,11 +649,11 @@ interface MastodonApi {
|
||||||
@Header("Authorization") auth: String,
|
@Header("Authorization") auth: String,
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@FieldMap data: Map<String, Boolean>
|
@FieldMap data: Map<String, Boolean>
|
||||||
): Result<NotificationSubscribeResult>
|
): NetworkResult<NotificationSubscribeResult>
|
||||||
|
|
||||||
@DELETE("api/v1/push/subscription")
|
@DELETE("api/v1/push/subscription")
|
||||||
suspend fun unsubscribePushNotifications(
|
suspend fun unsubscribePushNotifications(
|
||||||
@Header("Authorization") auth: String,
|
@Header("Authorization") auth: String,
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
): Result<ResponseBody>
|
): NetworkResult<ResponseBody>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import com.keylesspalace.tusky.entity.MediaUploadResult
|
import com.keylesspalace.tusky.entity.MediaUploadResult
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import retrofit2.http.Multipart
|
import retrofit2.http.Multipart
|
||||||
|
@ -15,5 +16,5 @@ interface MediaUploadApi {
|
||||||
suspend fun uploadMedia(
|
suspend fun uploadMedia(
|
||||||
@Part file: MultipartBody.Part,
|
@Part file: MultipartBody.Part,
|
||||||
@Part description: MultipartBody.Part? = null
|
@Part description: MultipartBody.Part? = null
|
||||||
): Result<MediaUploadResult>
|
): NetworkResult<MediaUploadResult>
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
|
|
|
@ -72,6 +72,6 @@ object PrefKeys {
|
||||||
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
|
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
|
||||||
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
|
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
|
||||||
|
|
||||||
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
|
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default.
|
||||||
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.keylesspalace.tusky.usecase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
|
import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.removeShortcut
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LogoutUsecase @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val db: AppDatabase,
|
||||||
|
private val accountManager: AccountManager,
|
||||||
|
private val draftHelper: DraftHelper
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current account out and clears all caches associated with it
|
||||||
|
* @return true if the user is logged in with other accounts, false if it was the only one
|
||||||
|
*/
|
||||||
|
suspend fun logout(): Boolean {
|
||||||
|
accountManager.activeAccount?.let { activeAccount ->
|
||||||
|
|
||||||
|
// invalidate the oauth token, if we have the client id & secret
|
||||||
|
// (could be missing if user logged in with a previous version of Tusky)
|
||||||
|
val clientId = activeAccount.clientId
|
||||||
|
val clientSecret = activeAccount.clientSecret
|
||||||
|
if (clientId != null && clientSecret != null) {
|
||||||
|
api.revokeOAuthToken(
|
||||||
|
clientId = clientId,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
token = activeAccount.accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable push notifications
|
||||||
|
disableUnifiedPushNotificationsForAccount(context, activeAccount)
|
||||||
|
|
||||||
|
// disable pull notifications
|
||||||
|
if (!NotificationHelper.areNotificationsEnabled(context, accountManager)) {
|
||||||
|
NotificationHelper.disablePullNotifications(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear notification channels
|
||||||
|
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, context)
|
||||||
|
|
||||||
|
// remove account from local AccountManager
|
||||||
|
val otherAccountAvailable = accountManager.logActiveAccountOut() != null
|
||||||
|
|
||||||
|
// clear the database - this could trigger network calls so do it last when all tokens are gone
|
||||||
|
db.timelineDao().removeAll(activeAccount.id)
|
||||||
|
db.conversationDao().deleteForAccount(activeAccount.id)
|
||||||
|
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
||||||
|
|
||||||
|
// remove shortcut associated with the account
|
||||||
|
removeShortcut(context, activeAccount)
|
||||||
|
|
||||||
|
return otherAccountAvailable
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.usecase
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
|
@ -29,6 +29,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||||
import com.keylesspalace.tusky.entity.DeletedStatus
|
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
import io.reactivex.rxjava3.kotlin.addTo
|
|
@ -1,23 +0,0 @@
|
||||||
package com.keylesspalace.tusky.util
|
|
||||||
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.HttpException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronously executes the call and returns the response encapsulated in a kotlin.Result.
|
|
||||||
* Since Result is an inline class it is not possible to do this with a Retrofit adapter unfortunately.
|
|
||||||
* More efficient then calling a suspending method with runBlocking
|
|
||||||
*/
|
|
||||||
fun <T> Call<T>.result(): Result<T> {
|
|
||||||
return try {
|
|
||||||
val response = execute()
|
|
||||||
val responseBody = response.body()
|
|
||||||
if (response.isSuccessful && responseBody != null) {
|
|
||||||
Result.success(responseBody)
|
|
||||||
} else {
|
|
||||||
Result.failure(HttpException(response))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if this throwable indicates an error causes by a 4xx/5xx server response and
|
||||||
|
* tries to retrieve the error message the server sent
|
||||||
|
* @return the error message, or null if this is no server error or it had no error message
|
||||||
|
*/
|
||||||
|
fun Throwable.getServerErrorMessage(): String? {
|
||||||
|
if (this is HttpException) {
|
||||||
|
val errorResponse = response()?.errorBody()?.string()
|
||||||
|
return if (!errorResponse.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
JSONObject(errorResponse).getString("error")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
|
@ -47,8 +47,8 @@ sealed class StatusViewData {
|
||||||
get() = status.id
|
get() = status.id
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
* Specifies whether the content of this post is long enough to be automatically
|
||||||
* all content regardless.
|
* collapsed or if it should show all content regardless.
|
||||||
*
|
*
|
||||||
* @return Whether the post is collapsible or never collapsed.
|
* @return Whether the post is collapsible or never collapsed.
|
||||||
*/
|
*/
|
||||||
|
@ -109,7 +109,7 @@ sealed class StatusViewData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
fun copyWIthCollapsed(isCollapsed: Boolean): Concrete {
|
fun copyWithCollapsed(isCollapsed: Boolean): Concrete {
|
||||||
return copy(isCollapsed = isCollapsed)
|
return copy(isCollapsed = isCollapsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package com.keylesspalace.tusky.viewmodel
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -31,6 +32,7 @@ import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.getServerErrorMessage
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
@ -38,9 +40,6 @@ import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -155,21 +154,7 @@ class EditProfileViewModel @Inject constructor(
|
||||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
if (throwable is HttpException) {
|
saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage()))
|
||||||
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 {
|
|
||||||
saveData.postValue(Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.replacedFirstWhich
|
import com.keylesspalace.tusky.util.replacedFirstWhich
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
package net.accelf.yuito;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import com.google.android.material.textfield.TextInputEditText;
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import okhttp3.Call;
|
|
||||||
import okhttp3.Callback;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class AccessTokenLoginActivity extends AppCompatActivity implements Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountManager accountManager;
|
|
||||||
|
|
||||||
TextInputEditText domainEditText;
|
|
||||||
TextInputEditText accessTokenEditText;
|
|
||||||
TextView logTextView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_access_token_login);
|
|
||||||
|
|
||||||
domainEditText = findViewById(R.id.domainEditText);
|
|
||||||
accessTokenEditText = findViewById(R.id.accessTokenEditText);
|
|
||||||
Button authorizeButton = findViewById(R.id.authorizeButton);
|
|
||||||
logTextView = findViewById(R.id.logTextView);
|
|
||||||
|
|
||||||
authorizeButton.setOnClickListener(v -> authorize());
|
|
||||||
log("Input domain and access token to login.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void log(String text) {
|
|
||||||
runOnUiThread(() -> logTextView.setText(String.format("%s\n%s", logTextView.getText().toString(), text)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void authorize() {
|
|
||||||
if (domainEditText.getText() != null) {
|
|
||||||
String domain = domainEditText.getText().toString();
|
|
||||||
String accessToken = accessTokenEditText.getText().toString();
|
|
||||||
HttpUrl url;
|
|
||||||
|
|
||||||
log("Starting login test. [domain: " + domain + ", accessToken: " + accessToken + "]");
|
|
||||||
|
|
||||||
try {
|
|
||||||
url = new HttpUrl.Builder().host(domain).scheme("https")
|
|
||||||
.addPathSegments("/api/v1/accounts/verify_credentials")
|
|
||||||
.addQueryParameter("access_token", accessToken)
|
|
||||||
.build();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log("Wrong domain format. " + e.getMessage());
|
|
||||||
log("Aborting.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Access start -> " + url.toString());
|
|
||||||
|
|
||||||
OkHttpClient okHttpClient = new OkHttpClient();
|
|
||||||
Request request = new Request.Builder().url(url).get().build();
|
|
||||||
okHttpClient.newCall(request).enqueue(new Callback() {
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
||||||
log("Login failed. " + e.getMessage());
|
|
||||||
log("Aborting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
||||||
if (response.body() != null) {
|
|
||||||
log(response.body().string());
|
|
||||||
}
|
|
||||||
if (response.code() != 200) {
|
|
||||||
throw new IOException("Invalid response code. Response code was " + response.code());
|
|
||||||
}
|
|
||||||
log("Login successful. Moving to account registration phase.");
|
|
||||||
authSucceeded(domain, accessToken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void authSucceeded(String domain, String accessToken) {
|
|
||||||
accountManager.addAccount(accessToken, domain, "");
|
|
||||||
log("Completed. Enjoy!");
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package net.accelf.yuito
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
|
import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
|
import com.keylesspalace.tusky.MainActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ActivityAccessTokenLoginBinding
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AccessTokenLoginActivity : AppCompatActivity(), Injectable {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var mastodonApi: MastodonApi
|
||||||
|
|
||||||
|
private val binding by viewBinding(ActivityAccessTokenLoginBinding::inflate)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
binding.authorizeButton.setOnClickListener {
|
||||||
|
it.isEnabled = false
|
||||||
|
runBlocking { authorize() }
|
||||||
|
it.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Input domain and access token to login.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log(text: String) {
|
||||||
|
runOnUiThread {
|
||||||
|
binding.logTextView.text = String.format("%s\n%s", binding.logTextView.text.toString(), text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun authorize() {
|
||||||
|
if (binding.domainEditText.text.isNullOrBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val domain = binding.domainEditText.text.toString()
|
||||||
|
val accessToken = binding.accessTokenEditText.text.toString()
|
||||||
|
log("Starting login test. [domain: $domain, accessToken: $accessToken]")
|
||||||
|
mastodonApi.accountVerifyCredentials(domain, auth = "Bearer $accessToken")
|
||||||
|
.onSuccess { account ->
|
||||||
|
log("Login successful. Moving to account registration phase.")
|
||||||
|
accountManager.addAccount(accessToken, domain, "", "", "", account)
|
||||||
|
log("Completed. Enjoy!")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
.onFailure { e ->
|
||||||
|
log("Login failed. ${e.message}")
|
||||||
|
log("Aborting.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,9 @@ import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
|
import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemDrawerFooterBinding
|
import com.keylesspalace.tusky.databinding.ItemDrawerFooterBinding
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
|
@ -34,10 +37,10 @@ class FooterDrawerItem : AbstractDrawerItem<FooterDrawerItem, BindingHolder<Item
|
||||||
|
|
||||||
override fun getViewHolder(v: View): BindingHolder<ItemDrawerFooterBinding> = throw UnsupportedOperationException()
|
override fun getViewHolder(v: View): BindingHolder<ItemDrawerFooterBinding> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
fun setInstance(instance: Result<Instance>) {
|
fun setInstance(instance: NetworkResult<Instance>) {
|
||||||
instance
|
instance
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
binding.instanceData.text = String.format("%s\n%s\n%s", it.title, it.uri, it.version)
|
binding.instanceData.text = listOf(it.title, it.uri, it.version).joinToString("\n")
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
|
binding.instanceData.text = binding.root.context.getString(R.string.instance_data_failed)
|
||||||
|
|
|
@ -297,6 +297,7 @@
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/composeBottomBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
|
|
|
@ -14,11 +14,13 @@
|
||||||
tools:context="com.keylesspalace.tusky.MainActivity">
|
tools:context="com.keylesspalace.tusky.MainActivity">
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:id="@+id/mainCoordinatorLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1">
|
android:layout_weight="1">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:elevation="@dimen/actionbar_elevation"
|
android:elevation="@dimen/actionbar_elevation"
|
||||||
|
@ -81,6 +83,13 @@
|
||||||
|
|
||||||
<include layout="@layout/item_status_bottom_sheet" />
|
<include layout="@layout/item_status_bottom_sheet" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<net.accelf.yuito.QuickTootView
|
<net.accelf.yuito.QuickTootView
|
||||||
|
|
|
@ -332,6 +332,16 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
||||||
app:srcCompat="@drawable/ic_reply_24dp" />
|
app:srcCompat="@drawable/ic_reply_24dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_replies"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/status_reply"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/status_reply"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/status_reply"
|
||||||
|
android:textAlignment="viewEnd"
|
||||||
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
<at.connyduck.sparkbutton.SparkButton
|
<at.connyduck.sparkbutton.SparkButton
|
||||||
android:id="@+id/status_inset"
|
android:id="@+id/status_inset"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
|
|
|
@ -198,7 +198,7 @@
|
||||||
<string name="pref_default_post_privacy">Výchozí soukromí příspěvků</string>
|
<string name="pref_default_post_privacy">Výchozí soukromí příspěvků</string>
|
||||||
<string name="pref_default_media_sensitivity">Vždy označovat média jako citlivá</string>
|
<string name="pref_default_media_sensitivity">Vždy označovat média jako citlivá</string>
|
||||||
<string name="pref_publishing">Publikování (synchronizováno se serverem)</string>
|
<string name="pref_publishing">Publikování (synchronizováno se serverem)</string>
|
||||||
<string name="pref_failed_to_sync">Nepodařilo se synchronizovsat nastavení</string>
|
<string name="pref_failed_to_sync">Nepodařilo se synchronizovat nastavení</string>
|
||||||
<string name="post_privacy_public">Veřejné</string>
|
<string name="post_privacy_public">Veřejné</string>
|
||||||
<string name="post_privacy_unlisted">Neuvedené</string>
|
<string name="post_privacy_unlisted">Neuvedené</string>
|
||||||
<string name="post_privacy_followers_only">Pouze pro sledující</string>
|
<string name="post_privacy_followers_only">Pouze pro sledující</string>
|
||||||
|
@ -483,4 +483,12 @@
|
||||||
<string name="pref_title_confirm_reblogs">Zobrazit dialogové okno s potvrzením při boostování</string>
|
<string name="pref_title_confirm_reblogs">Zobrazit dialogové okno s potvrzením při boostování</string>
|
||||||
<string name="notification_subscription_format">%s právě vydal</string>
|
<string name="notification_subscription_format">%s právě vydal</string>
|
||||||
<string name="title_announcements">Oznámení</string>
|
<string name="title_announcements">Oznámení</string>
|
||||||
|
<string name="title_login">Přihlášení</string>
|
||||||
|
<string name="notification_sign_up_format">%s se zaregistroval</string>
|
||||||
|
<string name="title_migration_relogin">Přihlaste se znovu pro oznámení</string>
|
||||||
|
<string name="error_could_not_load_login_page">Nepodařilo se načíst stránku přihlášení.</string>
|
||||||
|
<string name="drafts_post_failed_to_send">Tento příspěvek se nepodařilo poslat!</string>
|
||||||
|
<string name="error_loading_account_details">Nepodařilo se načíst detaily účtu</string>
|
||||||
|
<string name="drafts_failed_loading_reply">Nepodařilo se načíst informace o odpovědi</string>
|
||||||
|
<string name="error_image_edit_failed">Obrázek se nepodařilo upravit.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -536,4 +536,9 @@
|
||||||
<string name="title_login">Anmelden</string>
|
<string name="title_login">Anmelden</string>
|
||||||
<string name="error_could_not_load_login_page">Die Anmeldeseite konnte nicht geladen werden.</string>
|
<string name="error_could_not_load_login_page">Die Anmeldeseite konnte nicht geladen werden.</string>
|
||||||
<string name="notification_update_name">Beitragsbearbeitungen</string>
|
<string name="notification_update_name">Beitragsbearbeitungen</string>
|
||||||
|
<string name="title_migration_relogin">Neuanmeldung für Push-Benachrichtigungen</string>
|
||||||
|
<string name="action_dismiss">Ablehnen</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Du hast dich erneut in dein aktuelles Konto eingeloggt, um Tusky die Genehmigung für Push-Abonnements zu erteilen. Du hast jedoch noch andere Konten, die nicht auf diese Weise migriert wurden. Wechsel zu diesen Konten und melde dich nacheinander neu an, um die Unterstützung für UnifiedPush-Benachrichtigungen zu aktivieren.</string>
|
||||||
|
<string name="dialog_push_notification_migration">Um Push-Benachrichtigungen über UnifiedPush verwenden zu können, benötigt Tusky die Erlaubnis, Benachrichtigungen auf Ihrem Mastodon-Server zu abonnieren. Dies erfordert eine erneute Anmeldung, um die Tusky gewährten OAuth-Bereiche zu ändern. Wenn du die Option zum erneuten Einloggen hier oder in den Kontoeinstellungen verwendest, bleiben alle deine lokalen Entwürfe und der Cache erhalten.</string>
|
||||||
|
<string name="tips_push_notification_migration">Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -8,22 +8,22 @@
|
||||||
<string name="error_authorization_unknown">خطای احراز هویت ناشناختهای رخ داد.</string>
|
<string name="error_authorization_unknown">خطای احراز هویت ناشناختهای رخ داد.</string>
|
||||||
<string name="error_authorization_denied">احراز هویت رد شد.</string>
|
<string name="error_authorization_denied">احراز هویت رد شد.</string>
|
||||||
<string name="error_retrieving_oauth_token">دریافت ژتون ورود شکست خورد.</string>
|
<string name="error_retrieving_oauth_token">دریافت ژتون ورود شکست خورد.</string>
|
||||||
<string name="error_compose_character_limit">وضعیت خیلی طولانی است!</string>
|
<string name="error_compose_character_limit">فرسته خیلی طولانی است!</string>
|
||||||
<string name="error_image_upload_size">پرونده باید کمتر از ۸ مگابایت باشد.</string>
|
<string name="error_image_upload_size">پرونده باید کمتر از ۸ مگابایت باشد.</string>
|
||||||
<string name="error_video_upload_size">پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد.</string>
|
<string name="error_video_upload_size">پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد.</string>
|
||||||
<string name="error_media_upload_type">این گونهٔ پرونده نمیتواند بارگذاری شود.</string>
|
<string name="error_media_upload_type">این گونهٔ پرونده نمیتواند بارگذاری شود.</string>
|
||||||
<string name="error_media_upload_opening">این پرونده نتوانست گشوده شود.</string>
|
<string name="error_media_upload_opening">این پرونده نتوانست گشوده شود.</string>
|
||||||
<string name="error_media_upload_permission">نیاز به اجازهٔ خواندن رسانه است.</string>
|
<string name="error_media_upload_permission">نیاز به اجازهٔ خواندن رسانه است.</string>
|
||||||
<string name="error_media_download_permission">نیاز به اجازهٔ ذخیرهٔ رسانه است.</string>
|
<string name="error_media_download_permission">نیاز به اجازهٔ ذخیرهٔ رسانه است.</string>
|
||||||
<string name="error_media_upload_image_or_video">تصاویر و فیلمها هر دو نمیتوانند به یک وضعیت ضمیمه شوند.</string>
|
<string name="error_media_upload_image_or_video">تصاویر و فیلمها نمیتوانند به یک فرسته پیوست شوند.</string>
|
||||||
<string name="error_media_upload_sending">بارگذاری شکست خورد.</string>
|
<string name="error_media_upload_sending">بارگذاری شکست خورد.</string>
|
||||||
<string name="error_sender_account_gone">خطای فرستادن بوق.</string>
|
<string name="error_sender_account_gone">خطای فرستادن فرسته.</string>
|
||||||
<string name="title_home">خانه</string>
|
<string name="title_home">خانه</string>
|
||||||
<string name="title_notifications">آگاهیها</string>
|
<string name="title_notifications">آگاهیها</string>
|
||||||
<string name="title_public_local">محلّی</string>
|
<string name="title_public_local">محلّی</string>
|
||||||
<string name="title_public_federated">همگانی</string>
|
<string name="title_public_federated">همگانی</string>
|
||||||
<string name="title_view_thread">بوق</string>
|
<string name="title_view_thread">رشته</string>
|
||||||
<string name="title_posts">فرسته</string>
|
<string name="title_posts">فرستهها</string>
|
||||||
<string name="title_posts_with_replies">با پاسخ</string>
|
<string name="title_posts_with_replies">با پاسخ</string>
|
||||||
<string name="title_follows">دنبال شونده</string>
|
<string name="title_follows">دنبال شونده</string>
|
||||||
<string name="title_followers">پیگیر</string>
|
<string name="title_followers">پیگیر</string>
|
||||||
|
@ -41,10 +41,10 @@
|
||||||
<string name="post_content_warning_show_more">نمایش بیشتر</string>
|
<string name="post_content_warning_show_more">نمایش بیشتر</string>
|
||||||
<string name="post_content_warning_show_less">نمایش کمتر</string>
|
<string name="post_content_warning_show_less">نمایش کمتر</string>
|
||||||
<string name="post_content_show_more">گسترش</string>
|
<string name="post_content_show_more">گسترش</string>
|
||||||
<string name="post_content_show_less">بستن</string>
|
<string name="post_content_show_less">جمع کردن</string>
|
||||||
<string name="footer_empty">اینجا هیچچیز نیست. برای تازهسازی، به پایین بکشید!</string>
|
<string name="footer_empty">اینجا هیچچیز نیست. برای تازهسازی، به پایین بکشید!</string>
|
||||||
<string name="notification_reblog_format">%s بوقتان را تقویت کرد</string>
|
<string name="notification_reblog_format">%s فرستهتان را تقویت کرد</string>
|
||||||
<string name="notification_favourite_format">%s بوقتان را برگزید</string>
|
<string name="notification_favourite_format">%s فرستهتان را برگزید</string>
|
||||||
<string name="notification_follow_format">%s پیگیرتان شد</string>
|
<string name="notification_follow_format">%s پیگیرتان شد</string>
|
||||||
<string name="report_username_format">گزارش @%s</string>
|
<string name="report_username_format">گزارش @%s</string>
|
||||||
<string name="report_comment_hint">نظرهای اضافی؟</string>
|
<string name="report_comment_hint">نظرهای اضافی؟</string>
|
||||||
|
@ -93,13 +93,13 @@
|
||||||
<string name="action_reject">رد</string>
|
<string name="action_reject">رد</string>
|
||||||
<string name="action_search">جستوجو</string>
|
<string name="action_search">جستوجو</string>
|
||||||
<string name="action_access_drafts">پیشنویسها</string>
|
<string name="action_access_drafts">پیشنویسها</string>
|
||||||
<string name="action_toggle_visibility">نمایانی بوق</string>
|
<string name="action_toggle_visibility">نمایانی فرسته</string>
|
||||||
<string name="action_content_warning">هشدار محتوا</string>
|
<string name="action_content_warning">هشدار محتوا</string>
|
||||||
<string name="action_emoji_keyboard">صفحهکلید اموجی</string>
|
<string name="action_emoji_keyboard">صفحهکلید اموجی</string>
|
||||||
<string name="download_image">درحال بارگیری %1$s</string>
|
<string name="download_image">درحال بارگیری %1$s</string>
|
||||||
<string name="action_copy_link">رونوشت از پیوند</string>
|
<string name="action_copy_link">رونوشت از پیوند</string>
|
||||||
<string name="send_post_link_to">همرسانی نشانی بوق با…</string>
|
<string name="send_post_link_to">همرسانی نشانی فرسته با…</string>
|
||||||
<string name="send_post_content_to">همرسانی بوق با…</string>
|
<string name="send_post_content_to">همرسانی فرسته با…</string>
|
||||||
<string name="send_media_to">همرسانی رسانه با…</string>
|
<string name="send_media_to">همرسانی رسانه با…</string>
|
||||||
<string name="confirmation_reported">فرستاده شد!</string>
|
<string name="confirmation_reported">فرستاده شد!</string>
|
||||||
<string name="confirmation_unblocked">کاربرنامسدود شد</string>
|
<string name="confirmation_unblocked">کاربرنامسدود شد</string>
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
<string name="dialog_download_image">بارگیری</string>
|
<string name="dialog_download_image">بارگیری</string>
|
||||||
<string name="dialog_message_cancel_follow_request">درخواست دنبال کردن را لغو میکنید؟</string>
|
<string name="dialog_message_cancel_follow_request">درخواست دنبال کردن را لغو میکنید؟</string>
|
||||||
<string name="dialog_unfollow_warning">ناپیگیری این حساب؟</string>
|
<string name="dialog_unfollow_warning">ناپیگیری این حساب؟</string>
|
||||||
<string name="dialog_delete_post_warning">حذف این بوق؟</string>
|
<string name="dialog_delete_post_warning">حذف این فرسته؟</string>
|
||||||
<string name="visibility_public">عمومی: فرستادن به خط زمانیهای عمومی</string>
|
<string name="visibility_public">عمومی: فرستادن به خط زمانیهای عمومی</string>
|
||||||
<string name="visibility_unlisted">فهرستنشده: نشان ندادن در خط زمانیهای عمومی</string>
|
<string name="visibility_unlisted">فهرستنشده: نشان ندادن در خط زمانیهای عمومی</string>
|
||||||
<string name="visibility_private">تنها دنبالکنندگان:پست فقط به دنبالکنندگان</string>
|
<string name="visibility_private">تنها دنبالکنندگان:پست فقط به دنبالکنندگان</string>
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
<string name="pref_title_browser_settings">مرورگر</string>
|
<string name="pref_title_browser_settings">مرورگر</string>
|
||||||
<string name="pref_title_custom_tabs">استفاده از زبانههای سفارشی کروم</string>
|
<string name="pref_title_custom_tabs">استفاده از زبانههای سفارشی کروم</string>
|
||||||
<string name="pref_title_hide_follow_button">نهفتن دکمهٔ ایجاد، هنگام پیمایش</string>
|
<string name="pref_title_hide_follow_button">نهفتن دکمهٔ ایجاد، هنگام پیمایش</string>
|
||||||
<string name="pref_title_post_filter">فیلتر کردن خط زمانی</string>
|
<string name="pref_title_post_filter">پالایش خط زمانی</string>
|
||||||
<string name="pref_title_post_tabs">زبانهها</string>
|
<string name="pref_title_post_tabs">زبانهها</string>
|
||||||
<string name="pref_title_show_boosts">نمایش تقویتها</string>
|
<string name="pref_title_show_boosts">نمایش تقویتها</string>
|
||||||
<string name="pref_title_show_replies">نمایش پاسخها</string>
|
<string name="pref_title_show_replies">نمایش پاسخها</string>
|
||||||
|
@ -173,10 +173,10 @@
|
||||||
<string name="post_privacy_public">عمومی</string>
|
<string name="post_privacy_public">عمومی</string>
|
||||||
<string name="post_privacy_unlisted">فهرستنشده</string>
|
<string name="post_privacy_unlisted">فهرستنشده</string>
|
||||||
<string name="post_privacy_followers_only">فقط پیگیران</string>
|
<string name="post_privacy_followers_only">فقط پیگیران</string>
|
||||||
<string name="pref_post_text_size">اندازهٔ متن وضعیت</string>
|
<string name="pref_post_text_size">اندازهٔ متن فرسته</string>
|
||||||
<string name="post_text_size_smallest">کوچکترین</string>
|
<string name="post_text_size_smallest">کوچکترین</string>
|
||||||
<string name="post_text_size_small">کوچک</string>
|
<string name="post_text_size_small">کوچک</string>
|
||||||
<string name="post_text_size_medium">متوسط</string>
|
<string name="post_text_size_medium">میانه</string>
|
||||||
<string name="post_text_size_large">بزرگ</string>
|
<string name="post_text_size_large">بزرگ</string>
|
||||||
<string name="post_text_size_largest">بزرگترین</string>
|
<string name="post_text_size_largest">بزرگترین</string>
|
||||||
<string name="notification_mention_name">اشارههای جدید</string>
|
<string name="notification_mention_name">اشارههای جدید</string>
|
||||||
|
@ -184,9 +184,9 @@
|
||||||
<string name="notification_follow_name">پیگیران جدید</string>
|
<string name="notification_follow_name">پیگیران جدید</string>
|
||||||
<string name="notification_follow_description">آگاهیها دربارهٔ پیگیران جدید</string>
|
<string name="notification_follow_description">آگاهیها دربارهٔ پیگیران جدید</string>
|
||||||
<string name="notification_boost_name">تقویتها</string>
|
<string name="notification_boost_name">تقویتها</string>
|
||||||
<string name="notification_boost_description">آگاهیها هنگام تقویت شدن بوقهایتان</string>
|
<string name="notification_boost_description">آگاهیها هنگام تقویت فرستههایتان</string>
|
||||||
<string name="notification_favourite_name">برگزیدنها</string>
|
<string name="notification_favourite_name">برگزیدنها</string>
|
||||||
<string name="notification_favourite_description">آگاهیها هنگام برگزیده شدن بوقهایتان</string>
|
<string name="notification_favourite_description">آگاهیها هنگام برگزیده شدن فرستههایتان</string>
|
||||||
<string name="notification_mention_format">%s به شما اشاره کرد</string>
|
<string name="notification_mention_format">%s به شما اشاره کرد</string>
|
||||||
<string name="notification_summary_large">%1$s، %2$s، %3$s و %4$d دیگر</string>
|
<string name="notification_summary_large">%1$s، %2$s، %3$s و %4$d دیگر</string>
|
||||||
<string name="notification_summary_medium">%1$s، %2$s و %3$s</string>
|
<string name="notification_summary_medium">%1$s، %2$s و %3$s</string>
|
||||||
|
@ -209,8 +209,8 @@
|
||||||
<string name="about_bug_feature_request_site">گزارش مشکلات و درخواست ویژگیها:
|
<string name="about_bug_feature_request_site">گزارش مشکلات و درخواست ویژگیها:
|
||||||
\n https://github.com/accelforce/Yuito/issues</string>
|
\n https://github.com/accelforce/Yuito/issues</string>
|
||||||
<string name="about_tusky_account">نمایهٔ تاسکی</string>
|
<string name="about_tusky_account">نمایهٔ تاسکی</string>
|
||||||
<string name="post_share_content">همرسانی محتوای بوق</string>
|
<string name="post_share_content">همرسانی محتوای فرسته</string>
|
||||||
<string name="post_share_link">همرسانی پیوند بوق</string>
|
<string name="post_share_link">همرسانی پیوند فرسته</string>
|
||||||
<string name="post_media_images">تصویرها</string>
|
<string name="post_media_images">تصویرها</string>
|
||||||
<string name="post_media_video">ویدیو</string>
|
<string name="post_media_video">ویدیو</string>
|
||||||
<string name="state_follow_requested">تقاضای پیگیری شد</string>
|
<string name="state_follow_requested">تقاضای پیگیری شد</string>
|
||||||
|
@ -240,19 +240,19 @@
|
||||||
<string name="lock_account_label">قفل حساب</string>
|
<string name="lock_account_label">قفل حساب</string>
|
||||||
<string name="lock_account_label_description">لازم است پیگیران را دستی تأیید کنید</string>
|
<string name="lock_account_label_description">لازم است پیگیران را دستی تأیید کنید</string>
|
||||||
<string name="compose_save_draft">ذخیرهٔ پیشنویس؟</string>
|
<string name="compose_save_draft">ذخیرهٔ پیشنویس؟</string>
|
||||||
<string name="send_post_notification_title">در حال فرستادن بوق…</string>
|
<string name="send_post_notification_title">فرستادن فرسته…</string>
|
||||||
<string name="send_post_notification_error_title">خطای فرستادن بوق</string>
|
<string name="send_post_notification_error_title">خطا در فرستادن فرسته</string>
|
||||||
<string name="send_post_notification_channel_name">در حال فرستادن بوقها</string>
|
<string name="send_post_notification_channel_name">فرستادن فرستهها</string>
|
||||||
<string name="send_post_notification_cancel_title">فرستادن لغو شد</string>
|
<string name="send_post_notification_cancel_title">فرستادن لغو شد</string>
|
||||||
<string name="send_post_notification_saved_content">رونوشتی از بوق در پیشنویسهایتان ذخیره شد</string>
|
<string name="send_post_notification_saved_content">رونوشتی از فرسته در پیشنویسهایتان ذخیره شد</string>
|
||||||
<string name="action_compose_shortcut">ایجاد</string>
|
<string name="action_compose_shortcut">ایجاد</string>
|
||||||
<string name="error_no_custom_emojis">نمونهتان %s هیچ اموجی سفارشیای ندارد</string>
|
<string name="error_no_custom_emojis">نمونهتان %s هیچ اموجی سفارشیای ندارد</string>
|
||||||
<string name="emoji_style">سبک اموجی</string>
|
<string name="emoji_style">سبک اموجی</string>
|
||||||
<string name="system_default">پیشگزیدهٔ سامانه</string>
|
<string name="system_default">پیشگزیدهٔ سامانه</string>
|
||||||
<string name="download_fonts">نخست باید این مجموعههای اموجی را بارگیری کنید</string>
|
<string name="download_fonts">نخست باید این مجموعههای اموجی را بارگیری کنید</string>
|
||||||
<string name="performing_lookup_title">در حال جستوجو…</string>
|
<string name="performing_lookup_title">در حال جستوجو…</string>
|
||||||
<string name="expand_collapse_all_posts">گسترده/جمع کردن تمام وضعیتها</string>
|
<string name="expand_collapse_all_posts">گسترش/جمع کردن تمام فرستهها</string>
|
||||||
<string name="action_open_post">گشودن بوق</string>
|
<string name="action_open_post">گشودن فرسته</string>
|
||||||
<string name="restart_required">نیاز به آغاز دوبارهٔ کاره</string>
|
<string name="restart_required">نیاز به آغاز دوبارهٔ کاره</string>
|
||||||
<string name="restart_emoji">برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید</string>
|
<string name="restart_emoji">برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید</string>
|
||||||
<string name="later">بعداً</string>
|
<string name="later">بعداً</string>
|
||||||
|
@ -278,7 +278,7 @@
|
||||||
<string name="error_network">یک خطای شبکه رخ داد! لطفا اتصال خود را بررسی و دوباره تلاش کنید!</string>
|
<string name="error_network">یک خطای شبکه رخ داد! لطفا اتصال خود را بررسی و دوباره تلاش کنید!</string>
|
||||||
<string name="title_direct_messages">پیامهای مستقیم</string>
|
<string name="title_direct_messages">پیامهای مستقیم</string>
|
||||||
<string name="title_tab_preferences">زبانهها</string>
|
<string name="title_tab_preferences">زبانهها</string>
|
||||||
<string name="title_posts_pinned">سنجاقشده</string>
|
<string name="title_posts_pinned">سنجاق شده</string>
|
||||||
<string name="title_domain_mutes">دامنههای نهفته</string>
|
<string name="title_domain_mutes">دامنههای نهفته</string>
|
||||||
<string name="post_username_format">\@%s</string>
|
<string name="post_username_format">\@%s</string>
|
||||||
<string name="message_empty">اینجا هیچچیزی نیست.</string>
|
<string name="message_empty">اینجا هیچچیزی نیست.</string>
|
||||||
|
@ -304,7 +304,7 @@
|
||||||
<string name="download_media">بارگیری رسانه</string>
|
<string name="download_media">بارگیری رسانه</string>
|
||||||
<string name="downloading_media">در حال بارگیری رسانه</string>
|
<string name="downloading_media">در حال بارگیری رسانه</string>
|
||||||
<string name="confirmation_domain_unmuted">%s نانهفته</string>
|
<string name="confirmation_domain_unmuted">%s نانهفته</string>
|
||||||
<string name="dialog_redraft_post_warning">میخواهید این بوق را پاک و بازنویسی کنید؟</string>
|
<string name="dialog_redraft_post_warning">حذف و بازنویسی این فرسته؟</string>
|
||||||
<string name="mute_domain_warning_dialog_ok">نهفتن تمام دامنه</string>
|
<string name="mute_domain_warning_dialog_ok">نهفتن تمام دامنه</string>
|
||||||
<string name="pref_title_notification_filter_poll">پایان نظرسنجیها</string>
|
<string name="pref_title_notification_filter_poll">پایان نظرسنجیها</string>
|
||||||
<string name="pref_title_timeline_filters">پالایهها</string>
|
<string name="pref_title_timeline_filters">پالایهها</string>
|
||||||
|
@ -320,7 +320,7 @@
|
||||||
<string name="abbreviated_hours_ago">%d ساعت</string>
|
<string name="abbreviated_hours_ago">%d ساعت</string>
|
||||||
<string name="abbreviated_minutes_ago">%d دقیقه</string>
|
<string name="abbreviated_minutes_ago">%d دقیقه</string>
|
||||||
<string name="abbreviated_seconds_ago">%d ثانیه</string>
|
<string name="abbreviated_seconds_ago">%d ثانیه</string>
|
||||||
<string name="pref_title_alway_open_spoiler">گسترش همیشگی بوقهای علامتخورده با هشدار محتوا</string>
|
<string name="pref_title_alway_open_spoiler">گسترش همیشگی فرستههای علامتخورده با هشدار محتوا</string>
|
||||||
<string name="pref_title_public_filter_keywords">خط زمانیهای عمومی</string>
|
<string name="pref_title_public_filter_keywords">خط زمانیهای عمومی</string>
|
||||||
<string name="pref_title_thread_filter_keywords">گفتوگوها</string>
|
<string name="pref_title_thread_filter_keywords">گفتوگوها</string>
|
||||||
<string name="filter_addition_dialog_title">افزودن پالایه</string>
|
<string name="filter_addition_dialog_title">افزودن پالایه</string>
|
||||||
|
@ -360,8 +360,8 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="description_post_media">رسانه: %s</string>
|
<string name="description_post_media">رسانه: %s</string>
|
||||||
<string name="description_post_cw">هشدار محتوا: %s</string>
|
<string name="description_post_cw">هشدار محتوا: %s</string>
|
||||||
<string name="description_post_media_no_description_placeholder">بدون هیچ توضیحی</string>
|
<string name="description_post_media_no_description_placeholder">بدون شرح</string>
|
||||||
<string name="description_post_reblogged">بازبوقیده</string>
|
<string name="description_post_reblogged">تقویت شده</string>
|
||||||
<string name="description_post_favourited">برگزیده</string>
|
<string name="description_post_favourited">برگزیده</string>
|
||||||
<string name="description_visiblity_public">عمومی</string>
|
<string name="description_visiblity_public">عمومی</string>
|
||||||
<string name="description_visiblity_unlisted">فهرستنشده</string>
|
<string name="description_visiblity_unlisted">فهرستنشده</string>
|
||||||
|
@ -373,7 +373,7 @@
|
||||||
<string name="notifications_clear">پاکسازی</string>
|
<string name="notifications_clear">پاکسازی</string>
|
||||||
<string name="notifications_apply_filter">پالایش</string>
|
<string name="notifications_apply_filter">پالایش</string>
|
||||||
<string name="filter_apply">اعمال</string>
|
<string name="filter_apply">اعمال</string>
|
||||||
<string name="compose_shortcut_long_label">ایجاد بوق</string>
|
<string name="compose_shortcut_long_label">ایجاد فرسته</string>
|
||||||
<string name="compose_shortcut_short_label">ایجاد</string>
|
<string name="compose_shortcut_short_label">ایجاد</string>
|
||||||
<string name="notification_clear_text">مطمئنید میخواهید تمام آگاهیهایتان را برای همیشه پاک کنید؟</string>
|
<string name="notification_clear_text">مطمئنید میخواهید تمام آگاهیهایتان را برای همیشه پاک کنید؟</string>
|
||||||
<string name="poll_info_time_absolute">پایان در %s</string>
|
<string name="poll_info_time_absolute">پایان در %s</string>
|
||||||
|
@ -400,7 +400,7 @@
|
||||||
<string name="hint_additional_info">نظرهای اضافی</string>
|
<string name="hint_additional_info">نظرهای اضافی</string>
|
||||||
<string name="report_remote_instance">هدایت به %s</string>
|
<string name="report_remote_instance">هدایت به %s</string>
|
||||||
<string name="failed_report">شکست در گزارش</string>
|
<string name="failed_report">شکست در گزارش</string>
|
||||||
<string name="failed_fetch_posts">شکست در واکشی وضعیتها</string>
|
<string name="failed_fetch_posts">شکست در واکشی فرستهها</string>
|
||||||
<string name="title_accounts">حسابها</string>
|
<string name="title_accounts">حسابها</string>
|
||||||
<string name="failed_search">شکست در جستوجو</string>
|
<string name="failed_search">شکست در جستوجو</string>
|
||||||
<string name="pref_title_show_notifications_filter">نمایش پالایهٔ آگاهیها</string>
|
<string name="pref_title_show_notifications_filter">نمایش پالایهٔ آگاهیها</string>
|
||||||
|
@ -416,10 +416,10 @@
|
||||||
<string name="poll_allow_multiple_choices">گزینههای چندگانه</string>
|
<string name="poll_allow_multiple_choices">گزینههای چندگانه</string>
|
||||||
<string name="poll_new_choice_hint">گزینهٔ %d</string>
|
<string name="poll_new_choice_hint">گزینهٔ %d</string>
|
||||||
<string name="edit_poll">ویرایش</string>
|
<string name="edit_poll">ویرایش</string>
|
||||||
<string name="title_scheduled_posts">بوقهای زمانبسته</string>
|
<string name="title_scheduled_posts">فرستههای زمانبسته</string>
|
||||||
<string name="action_edit">ویرایش</string>
|
<string name="action_edit">ویرایش</string>
|
||||||
<string name="action_access_scheduled_posts">بوقهای زمانبسته</string>
|
<string name="action_access_scheduled_posts">فرستههای زمانبسته</string>
|
||||||
<string name="action_schedule_post">بوق زمانبسته</string>
|
<string name="action_schedule_post">فرستهٔ زمانبسته</string>
|
||||||
<string name="action_reset_schedule">بازنشانی</string>
|
<string name="action_reset_schedule">بازنشانی</string>
|
||||||
<string name="mute_domain_warning">مطمئنید میخواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیگیرانتان از آن دامنه، برداشته خواهند شد.</string>
|
<string name="mute_domain_warning">مطمئنید میخواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیگیرانتان از آن دامنه، برداشته خواهند شد.</string>
|
||||||
<string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string>
|
<string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string>
|
||||||
|
@ -437,7 +437,7 @@
|
||||||
<string name="select_list_title">گزینش فهرست</string>
|
<string name="select_list_title">گزینش فهرست</string>
|
||||||
<string name="list">فهرست</string>
|
<string name="list">فهرست</string>
|
||||||
<string name="no_drafts">هیچ پیشنویسی ندارید.</string>
|
<string name="no_drafts">هیچ پیشنویسی ندارید.</string>
|
||||||
<string name="no_scheduled_posts">هیچ وضعیت زمانبستهای ندارید.</string>
|
<string name="no_scheduled_posts">هیچ فرستهٔ زمانبستهای ندارید.</string>
|
||||||
<string name="warning_scheduling_interval">ماستودون، بازهٔ زمانبندیای با کمینهٔ ۵ دقیقه دارد.</string>
|
<string name="warning_scheduling_interval">ماستودون، بازهٔ زمانبندیای با کمینهٔ ۵ دقیقه دارد.</string>
|
||||||
<string name="pref_title_confirm_reblogs">نمایش گفتوگوی تأیید، پیش از تقویت</string>
|
<string name="pref_title_confirm_reblogs">نمایش گفتوگوی تأیید، پیش از تقویت</string>
|
||||||
<string name="pref_title_show_cards_in_timelines">پیشنمایش پیوندها در خطزمانیها</string>
|
<string name="pref_title_show_cards_in_timelines">پیشنمایش پیوندها در خطزمانیها</string>
|
||||||
|
@ -482,7 +482,7 @@
|
||||||
<string name="action_unsubscribe_account">عدم اشتراک</string>
|
<string name="action_unsubscribe_account">عدم اشتراک</string>
|
||||||
<string name="action_subscribe_account">اشتراک</string>
|
<string name="action_subscribe_account">اشتراک</string>
|
||||||
<string name="draft_deleted">پیشنویس حذف شد</string>
|
<string name="draft_deleted">پیشنویس حذف شد</string>
|
||||||
<string name="drafts_post_failed_to_send">فرستادن این بوق شکست خورد!</string>
|
<string name="drafts_post_failed_to_send">فرستادن این فرسته شکست خورد!</string>
|
||||||
<string name="wellbeing_hide_stats_profile">نهفتن آمار کمی روی نمایهها</string>
|
<string name="wellbeing_hide_stats_profile">نهفتن آمار کمی روی نمایهها</string>
|
||||||
<string name="wellbeing_hide_stats_posts">نهفتن آمار کمی روی فرستهها</string>
|
<string name="wellbeing_hide_stats_posts">نهفتن آمار کمی روی فرستهها</string>
|
||||||
<string name="limit_notifications">محدود کردن آگاهیهای خطزمانی</string>
|
<string name="limit_notifications">محدود کردن آگاهیهای خطزمانی</string>
|
||||||
|
@ -491,17 +491,17 @@
|
||||||
<string name="label_duration">طول</string>
|
<string name="label_duration">طول</string>
|
||||||
<string name="post_media_attachments">پیوستها</string>
|
<string name="post_media_attachments">پیوستها</string>
|
||||||
<string name="post_media_audio">صدا</string>
|
<string name="post_media_audio">صدا</string>
|
||||||
<string name="notification_subscription_description">آگاهیها هنگام انتشار بوقی جدید از کسی که مشترکش هستید</string>
|
<string name="notification_subscription_description">آگاهیها هنگام انتشار فرستهای جدید از کسی که پیمیگیرید</string>
|
||||||
<string name="notification_subscription_name">بوقهای جدید</string>
|
<string name="notification_subscription_name">فرستههای جدید</string>
|
||||||
<string name="pref_title_animate_custom_emojis">اموجیهای شخصی متحرّک</string>
|
<string name="pref_title_animate_custom_emojis">اموجیهای شخصی متحرّک</string>
|
||||||
<string name="pref_title_notification_filter_subscriptions">کسی که مشترکش شدهام، بوقی جدید منتشر کرد</string>
|
<string name="pref_title_notification_filter_subscriptions">کسی که پیمیگیرم، فرستهای جدید منتشر کرد</string>
|
||||||
<string name="notification_subscription_format">%s چیزی فرستاد</string>
|
<string name="notification_subscription_format">%s چیزی فرستاد</string>
|
||||||
<string name="drafts_post_reply_removed">بوقی که پاسخی به آن را پیشنویس کردید، برداشته شده</string>
|
<string name="drafts_post_reply_removed">فرستهای که پاسخی به آن را پیشنویس کردید، برداشته شده</string>
|
||||||
<string name="drafts_failed_loading_reply">شکست در بار کردن اطّلاعات پاسخ</string>
|
<string name="drafts_failed_loading_reply">شکست در بار کردن اطّلاعات پاسخ</string>
|
||||||
<string name="wellbeing_mode_notice">برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون:
|
<string name="wellbeing_mode_notice">برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون:
|
||||||
\n
|
\n
|
||||||
\n - آگاهیهای برگزیدن، تقویت و پیگیری
|
\n - آگاهیهای برگزیدن، تقویت و پیگیری
|
||||||
\n - شمار برگزیدن و تقویت بوقها
|
\n - شمار برگزیدن و تقویت فرستهها
|
||||||
\n - آمار پیگیر و فرسته روی نمایهها
|
\n - آمار پیگیر و فرسته روی نمایهها
|
||||||
\n
|
\n
|
||||||
\n فرستادن آگاهیها تأثیر نمیپذیرد، ولی میتوانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید.</string>
|
\n فرستادن آگاهیها تأثیر نمیپذیرد، ولی میتوانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید.</string>
|
||||||
|
@ -514,4 +514,34 @@
|
||||||
<string name="follow_requests_info">با این که حسابتان قفل نیست، کارکنان %1$s فکر کردند ممکن است بخواهید درخواستهای پیگیری از این حسابها را دستی بازبینی کنید.</string>
|
<string name="follow_requests_info">با این که حسابتان قفل نیست، کارکنان %1$s فکر کردند ممکن است بخواهید درخواستهای پیگیری از این حسابها را دستی بازبینی کنید.</string>
|
||||||
<string name="dialog_delete_conversation_warning">حذف این گفتوگو؟</string>
|
<string name="dialog_delete_conversation_warning">حذف این گفتوگو؟</string>
|
||||||
<string name="action_delete_conversation">حذف گفتوگو</string>
|
<string name="action_delete_conversation">حذف گفتوگو</string>
|
||||||
|
<string name="account_date_joined">در %1$s پیوست</string>
|
||||||
|
<string name="title_login">ورود</string>
|
||||||
|
<string name="notification_sign_up_format">%s ثبتنام کرد</string>
|
||||||
|
<string name="pref_title_confirm_favourites">نمایش گفتوگوی تأیید پیش از برگزیدن</string>
|
||||||
|
<string name="tusky_compose_post_quicksetting_label">ایجاد فرسته</string>
|
||||||
|
<string name="tips_push_notification_migration">ورود دوباره به تمامی حسابها برای به کار انداختن پشتیبانی آگاهیهای ارسالی.</string>
|
||||||
|
<string name="notification_update_description">آگاهیها هنگام ویرایش فرستههایی که با آنها تعامل داشتهاید</string>
|
||||||
|
<string name="action_unbookmark">برداشن نشانک</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">برای اعطای اجازهٔ اشتراک آگاهیهای ارسالی ، دوباره به حسابتان وارد شدید. با این حال هنوز حسابهایی دیگر دارید که اینگونه مهاجرت داده نشدهاند. به آنها رفته و برای به کار انداختن پشتیبانی آگاهیهای UnifiedPush یکییکی دوباره وارد شوید.</string>
|
||||||
|
<string name="action_logout_confirm">مطمئنید که میخواهید از حساب %1$s خارج شوید؟</string>
|
||||||
|
<string name="duration_14_days">۱۴ روز</string>
|
||||||
|
<string name="duration_30_days">۳۰ روز</string>
|
||||||
|
<string name="duration_60_days">۶۰ روز</string>
|
||||||
|
<string name="duration_90_days">۹۰ روز</string>
|
||||||
|
<string name="duration_365_days">۳۶۵ روز</string>
|
||||||
|
<string name="duration_180_days">۱۸۰ روز</string>
|
||||||
|
<string name="status_count_one_plus">۱+</string>
|
||||||
|
<string name="dialog_push_notification_migration">تاسکی برای استفاده از آگاهیهای ارسالی با UnifiedPush نیاز به اجازهٔ اشتراک آگاهیها روی کارساز ماستودنتان دارد. این کار نیازمند ورود دوباره برای تغییر حوزههای OAuth اعطایی به تاسکی است. استفاده از گزینهٔ ورود دوباره در اینجا یا در ترجیحات حساب، تمامی انبارهها و پیشنویسهای محلیتان را نگه خواهد داشت.</string>
|
||||||
|
<string name="error_could_not_load_login_page">نتوانست صفحهٔ ورود را بار کند.</string>
|
||||||
|
<string name="pref_title_notification_filter_sign_ups">کسی ثبتنام کرد</string>
|
||||||
|
<string name="notification_update_name">ویرایشهای فرسته</string>
|
||||||
|
<string name="action_edit_image">ویرایش تصویر</string>
|
||||||
|
<string name="notification_update_format">%s فرستهاش را ویراست</string>
|
||||||
|
<string name="pref_title_notification_filter_updates">فرستهای که با آن تعامل داشتهام ویرایش شده</string>
|
||||||
|
<string name="notification_sign_up_name">ثبتنامها</string>
|
||||||
|
<string name="notification_sign_up_description">آگاهیها دربارهٔ کاربران جدید</string>
|
||||||
|
<string name="title_migration_relogin">ورود دوباره برای آگاهیهای ارسالی</string>
|
||||||
|
<string name="action_dismiss">رد کردن</string>
|
||||||
|
<string name="action_details">جزییات</string>
|
||||||
|
<string name="saving_draft">ذخیرهٔ پیشنویس…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<string name="title_tab_preferences">Onglets</string>
|
<string name="title_tab_preferences">Onglets</string>
|
||||||
<string name="title_view_thread">Fil</string>
|
<string name="title_view_thread">Fil</string>
|
||||||
<string name="title_posts">Messages</string>
|
<string name="title_posts">Messages</string>
|
||||||
<string name="title_posts_with_replies">Pouets & réponses</string>
|
<string name="title_posts_with_replies">Avec réponses</string>
|
||||||
<string name="title_posts_pinned">Épinglés</string>
|
<string name="title_posts_pinned">Épinglés</string>
|
||||||
<string name="title_follows">Abonnements</string>
|
<string name="title_follows">Abonnements</string>
|
||||||
<string name="title_followers">Abonné·e·s</string>
|
<string name="title_followers">Abonné·e·s</string>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<string name="post_boosted_format">%s a partagé</string>
|
<string name="post_boosted_format">%s a partagé</string>
|
||||||
<string name="post_sensitive_media_title">Contenu sensible</string>
|
<string name="post_sensitive_media_title">Contenu sensible</string>
|
||||||
<string name="post_media_hidden_title">Média caché</string>
|
<string name="post_media_hidden_title">Média caché</string>
|
||||||
<string name="post_sensitive_media_directions">Cliquer pour voir</string>
|
<string name="post_sensitive_media_directions">Appuyer pour voir</string>
|
||||||
<string name="post_content_warning_show_more">Voir plus</string>
|
<string name="post_content_warning_show_more">Voir plus</string>
|
||||||
<string name="post_content_warning_show_less">Voir moins</string>
|
<string name="post_content_warning_show_less">Voir moins</string>
|
||||||
<string name="post_content_show_more">Déplier</string>
|
<string name="post_content_show_more">Déplier</string>
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
<string name="dialog_download_image">Télécharger</string>
|
<string name="dialog_download_image">Télécharger</string>
|
||||||
<string name="dialog_message_cancel_follow_request">Révoquer la demande d’abonnement ?</string>
|
<string name="dialog_message_cancel_follow_request">Révoquer la demande d’abonnement ?</string>
|
||||||
<string name="dialog_unfollow_warning">Ne plus suivre ce compte ?</string>
|
<string name="dialog_unfollow_warning">Ne plus suivre ce compte ?</string>
|
||||||
<string name="dialog_delete_post_warning">Supprimer ce pouet ?</string>
|
<string name="dialog_delete_post_warning">Supprimer ce message \?</string>
|
||||||
<string name="visibility_public">Public : afficher dans les fils publics</string>
|
<string name="visibility_public">Public : afficher dans les fils publics</string>
|
||||||
<string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics</string>
|
<string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics</string>
|
||||||
<string name="visibility_private">Abonné·e·s uniquement : seul·e·s vos abonné·e·s verront vos statuts</string>
|
<string name="visibility_private">Abonné·e·s uniquement : seul·e·s vos abonné·e·s verront vos statuts</string>
|
||||||
|
@ -202,7 +202,7 @@
|
||||||
<string name="post_privacy_public">Public</string>
|
<string name="post_privacy_public">Public</string>
|
||||||
<string name="post_privacy_unlisted">Non listé</string>
|
<string name="post_privacy_unlisted">Non listé</string>
|
||||||
<string name="post_privacy_followers_only">Abonné·e·s uniquement</string>
|
<string name="post_privacy_followers_only">Abonné·e·s uniquement</string>
|
||||||
<string name="pref_post_text_size">Taille du texte pour les statuts</string>
|
<string name="pref_post_text_size">Taille du texte des messages</string>
|
||||||
<string name="post_text_size_smallest">Plus petit</string>
|
<string name="post_text_size_smallest">Plus petit</string>
|
||||||
<string name="post_text_size_small">Petit</string>
|
<string name="post_text_size_small">Petit</string>
|
||||||
<string name="post_text_size_medium">Moyen</string>
|
<string name="post_text_size_medium">Moyen</string>
|
||||||
|
@ -243,17 +243,17 @@
|
||||||
https://github.com/accelforce/Yuito/issues
|
https://github.com/accelforce/Yuito/issues
|
||||||
</string>
|
</string>
|
||||||
<string name="about_tusky_account">Profil de Yuito</string>
|
<string name="about_tusky_account">Profil de Yuito</string>
|
||||||
<string name="post_share_content">Partager le contenu du pouet</string>
|
<string name="post_share_content">Partager le contenu du message</string>
|
||||||
<string name="post_share_link">Partager le lien du pouet</string>
|
<string name="post_share_link">Partager le lien du message</string>
|
||||||
<string name="post_media_images">Images</string>
|
<string name="post_media_images">Images</string>
|
||||||
<string name="post_media_video">Vidéo</string>
|
<string name="post_media_video">Vidéo</string>
|
||||||
<string name="state_follow_requested">Demande d’abonnement effectuée</string>
|
<string name="state_follow_requested">Demande d’abonnement effectuée</string>
|
||||||
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
|
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
|
||||||
<string name="abbreviated_in_years">en %da</string>
|
<string name="abbreviated_in_years">dans %da</string>
|
||||||
<string name="abbreviated_in_days">en %dj</string>
|
<string name="abbreviated_in_days">dans %dj</string>
|
||||||
<string name="abbreviated_in_hours">en %d h</string>
|
<string name="abbreviated_in_hours">dans %dh</string>
|
||||||
<string name="abbreviated_in_minutes">en %dm</string>
|
<string name="abbreviated_in_minutes">dans %dm</string>
|
||||||
<string name="abbreviated_in_seconds">en %ds</string>
|
<string name="abbreviated_in_seconds">dans %ds</string>
|
||||||
<string name="abbreviated_years_ago">%d a</string>
|
<string name="abbreviated_years_ago">%d a</string>
|
||||||
<string name="abbreviated_days_ago">%dj</string>
|
<string name="abbreviated_days_ago">%dj</string>
|
||||||
<string name="abbreviated_hours_ago">%d h</string>
|
<string name="abbreviated_hours_ago">%d h</string>
|
||||||
|
@ -264,7 +264,7 @@
|
||||||
<string name="title_media">Média</string>
|
<string name="title_media">Média</string>
|
||||||
<string name="replying_to">Réponse à @%s</string>
|
<string name="replying_to">Réponse à @%s</string>
|
||||||
<string name="load_more_placeholder_text">en charger plus</string>
|
<string name="load_more_placeholder_text">en charger plus</string>
|
||||||
<string name="pref_title_public_filter_keywords">Timelines publiques</string>
|
<string name="pref_title_public_filter_keywords">Fils publics</string>
|
||||||
<string name="pref_title_thread_filter_keywords">Conversations</string>
|
<string name="pref_title_thread_filter_keywords">Conversations</string>
|
||||||
<string name="filter_addition_dialog_title">Ajouter un filtre</string>
|
<string name="filter_addition_dialog_title">Ajouter un filtre</string>
|
||||||
<string name="filter_edit_dialog_title">Modifier un filtre</string>
|
<string name="filter_edit_dialog_title">Modifier un filtre</string>
|
||||||
|
@ -296,19 +296,19 @@
|
||||||
<string name="lock_account_label">Verrouiller le compte</string>
|
<string name="lock_account_label">Verrouiller le compte</string>
|
||||||
<string name="lock_account_label_description">Vous devez approuver manuellement les abonnements</string>
|
<string name="lock_account_label_description">Vous devez approuver manuellement les abonnements</string>
|
||||||
<string name="compose_save_draft">Enregistrer comme brouillon ?</string>
|
<string name="compose_save_draft">Enregistrer comme brouillon ?</string>
|
||||||
<string name="send_post_notification_title">Envoi du pouet…</string>
|
<string name="send_post_notification_title">Envoi du message…</string>
|
||||||
<string name="send_post_notification_error_title">Erreur lors de l’envoi du pouet</string>
|
<string name="send_post_notification_error_title">Erreur lors de l’envoi du message</string>
|
||||||
<string name="send_post_notification_channel_name">Envoi des pouets</string>
|
<string name="send_post_notification_channel_name">Envoi des messages</string>
|
||||||
<string name="send_post_notification_cancel_title">Envoi annulé</string>
|
<string name="send_post_notification_cancel_title">Envoi annulé</string>
|
||||||
<string name="send_post_notification_saved_content">Une copie du pouet a été sauvegardée dans vos brouillons</string>
|
<string name="send_post_notification_saved_content">Une copie du message a été sauvegardée dans vos brouillons</string>
|
||||||
<string name="action_compose_shortcut">Écrire</string>
|
<string name="action_compose_shortcut">Écrire</string>
|
||||||
<string name="error_no_custom_emojis">Votre instance %s n’a pas d’émojis personnalisés</string>
|
<string name="error_no_custom_emojis">Votre instance %s n’a pas d’émojis personnalisés</string>
|
||||||
<string name="emoji_style">Style d’émojis</string>
|
<string name="emoji_style">Style d’émojis</string>
|
||||||
<string name="system_default">Par défaut du système</string>
|
<string name="system_default">Par défaut du système</string>
|
||||||
<string name="download_fonts">Vous devez commencer par télécharger ces jeux d’émojis</string>
|
<string name="download_fonts">Vous devez commencer par télécharger ces jeux d’émojis</string>
|
||||||
<string name="performing_lookup_title">Recherche en cours…</string>
|
<string name="performing_lookup_title">Recherche en cours…</string>
|
||||||
<string name="expand_collapse_all_posts">Déplier/replier tout les statuts</string>
|
<string name="expand_collapse_all_posts">Déplier/replier tout les messages</string>
|
||||||
<string name="action_open_post">Ouvrir le pouet</string>
|
<string name="action_open_post">Ouvrir le message</string>
|
||||||
<string name="restart_required">Un redémarrage de l’application est nécessaire</string>
|
<string name="restart_required">Un redémarrage de l’application est nécessaire</string>
|
||||||
<string name="restart_emoji">Vous devrez redémarrer Yuito pour appliquer ces modifications</string>
|
<string name="restart_emoji">Vous devrez redémarrer Yuito pour appliquer ces modifications</string>
|
||||||
<string name="later">Plus tard</string>
|
<string name="later">Plus tard</string>
|
||||||
|
@ -350,16 +350,12 @@
|
||||||
<item quantity="one">maximum de %1$d onglet atteint</item>
|
<item quantity="one">maximum de %1$d onglet atteint</item>
|
||||||
<item quantity="other">maximum de %1$d onglets atteint</item>
|
<item quantity="other">maximum de %1$d onglets atteint</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="description_post_media"> Média : %s
|
<string name="description_post_media">Média : %s</string>
|
||||||
</string>
|
|
||||||
<string name="description_post_cw"> Avertissement : %s
|
<string name="description_post_cw"> Avertissement : %s
|
||||||
</string>
|
</string>
|
||||||
<string name="description_post_media_no_description_placeholder"> Pas de description
|
<string name="description_post_media_no_description_placeholder">Aucune description</string>
|
||||||
</string>
|
<string name="description_post_reblogged">Partagé</string>
|
||||||
<string name="description_post_reblogged"> Reblogué
|
<string name="description_post_favourited">Mis en favoris</string>
|
||||||
</string>
|
|
||||||
<string name="description_post_favourited"> Mis en favoris
|
|
||||||
</string>
|
|
||||||
<string name="description_visiblity_public"> Public
|
<string name="description_visiblity_public"> Public
|
||||||
</string>
|
</string>
|
||||||
<string name="description_visiblity_unlisted">Non listé</string>
|
<string name="description_visiblity_unlisted">Non listé</string>
|
||||||
|
@ -377,7 +373,7 @@
|
||||||
<string name="pref_title_bot_overlay">Afficher l\'indicateur de robots</string>
|
<string name="pref_title_bot_overlay">Afficher l\'indicateur de robots</string>
|
||||||
<string name="notification_clear_text">Désirez-vous nettoyer toutes vos notifications de façon permanente \?</string>
|
<string name="notification_clear_text">Désirez-vous nettoyer toutes vos notifications de façon permanente \?</string>
|
||||||
<string name="action_delete_and_redraft">Effacer et ré-écrire</string>
|
<string name="action_delete_and_redraft">Effacer et ré-écrire</string>
|
||||||
<string name="dialog_redraft_post_warning">Effacer et ré-écrire ce pouet \?</string>
|
<string name="dialog_redraft_post_warning">Effacer et ré-écrire ce message \?</string>
|
||||||
<string name="poll_info_time_absolute">Termina à %s</string>
|
<string name="poll_info_time_absolute">Termina à %s</string>
|
||||||
<string name="poll_info_closed">Terminé</string>
|
<string name="poll_info_closed">Terminé</string>
|
||||||
<string name="poll_vote">Voter</string>
|
<string name="poll_vote">Voter</string>
|
||||||
|
@ -390,15 +386,15 @@
|
||||||
<item quantity="other">%d jours restants</item>
|
<item quantity="other">%d jours restants</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_hours">
|
<plurals name="poll_timespan_hours">
|
||||||
<item quantity="one">%d heure restant</item>
|
<item quantity="one">%d heure restante</item>
|
||||||
<item quantity="other">%d heures restantes</item>
|
<item quantity="other">%d heures restantes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_minutes">
|
<plurals name="poll_timespan_minutes">
|
||||||
<item quantity="one">%d minute restant</item>
|
<item quantity="one">%d minute restante</item>
|
||||||
<item quantity="other">%d minutes restantes</item>
|
<item quantity="other">%d minutes restantes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_seconds">
|
<plurals name="poll_timespan_seconds">
|
||||||
<item quantity="one">%d seconde restant</item>
|
<item quantity="one">%d seconde restante</item>
|
||||||
<item quantity="other">%d secondes restantes</item>
|
<item quantity="other">%d secondes restantes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="pref_title_animate_gif_avatars">Activer l’animation des avatars</string>
|
<string name="pref_title_animate_gif_avatars">Activer l’animation des avatars</string>
|
||||||
|
@ -418,7 +414,7 @@
|
||||||
<string name="hint_additional_info">Commentaires additionnels</string>
|
<string name="hint_additional_info">Commentaires additionnels</string>
|
||||||
<string name="report_remote_instance">Transférer à %s</string>
|
<string name="report_remote_instance">Transférer à %s</string>
|
||||||
<string name="failed_report">Échec du signalement</string>
|
<string name="failed_report">Échec du signalement</string>
|
||||||
<string name="failed_fetch_posts">Échec de récupération des statuts</string>
|
<string name="failed_fetch_posts">Échec de récupération des messages</string>
|
||||||
<string name="report_description_1">Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous :</string>
|
<string name="report_description_1">Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous :</string>
|
||||||
<string name="mute_domain_warning">Êtes-vous sûr⋅e de vouloir bloquer %s en entier \? Vous ne verrez plus de contenu provenant de ce domaine, ni dans les fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.</string>
|
<string name="mute_domain_warning">Êtes-vous sûr⋅e de vouloir bloquer %s en entier \? Vous ne verrez plus de contenu provenant de ce domaine, ni dans les fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.</string>
|
||||||
<string name="button_done">Terminé</string>
|
<string name="button_done">Terminé</string>
|
||||||
|
@ -444,8 +440,8 @@
|
||||||
<string name="edit_poll">Éditer</string>
|
<string name="edit_poll">Éditer</string>
|
||||||
<string name="title_scheduled_posts">Pouets planifiés</string>
|
<string name="title_scheduled_posts">Pouets planifiés</string>
|
||||||
<string name="action_edit">Éditer</string>
|
<string name="action_edit">Éditer</string>
|
||||||
<string name="action_access_scheduled_posts">Pouets programmés</string>
|
<string name="action_access_scheduled_posts">Messages programmés</string>
|
||||||
<string name="action_schedule_post">Planifier le pouet</string>
|
<string name="action_schedule_post">Planifier le message</string>
|
||||||
<string name="action_reset_schedule">Réinitialiser</string>
|
<string name="action_reset_schedule">Réinitialiser</string>
|
||||||
<string name="post_lookup_error_format">Erreur lors de la recherche du post %s</string>
|
<string name="post_lookup_error_format">Erreur lors de la recherche du post %s</string>
|
||||||
<string name="about_powered_by_tusky">Propulsé par Tusky</string>
|
<string name="about_powered_by_tusky">Propulsé par Tusky</string>
|
||||||
|
@ -457,7 +453,7 @@
|
||||||
<string name="list">Liste</string>
|
<string name="list">Liste</string>
|
||||||
<string name="error_audio_upload_size">Les fichiers audio doivent avoir moins de 40 Mo.</string>
|
<string name="error_audio_upload_size">Les fichiers audio doivent avoir moins de 40 Mo.</string>
|
||||||
<string name="no_drafts">Vous n’avez aucun brouillon.</string>
|
<string name="no_drafts">Vous n’avez aucun brouillon.</string>
|
||||||
<string name="no_scheduled_posts">Vous n’avez aucun pouet planifié.</string>
|
<string name="no_scheduled_posts">Vous n’avez aucun message planifié.</string>
|
||||||
<string name="warning_scheduling_interval">L’intervalle minimum de planification sur Mastodon est de 5 minutes.</string>
|
<string name="warning_scheduling_interval">L’intervalle minimum de planification sur Mastodon est de 5 minutes.</string>
|
||||||
<string name="notification_follow_request_name">Demandes d\'abonnement</string>
|
<string name="notification_follow_request_name">Demandes d\'abonnement</string>
|
||||||
<string name="dialog_block_warning">Bloquer @%s \?</string>
|
<string name="dialog_block_warning">Bloquer @%s \?</string>
|
||||||
|
@ -528,7 +524,7 @@
|
||||||
<string name="post_media_audio">Audio</string>
|
<string name="post_media_audio">Audio</string>
|
||||||
<string name="pref_title_confirm_favourites">Demander confirmation avant de mettre en favoris</string>
|
<string name="pref_title_confirm_favourites">Demander confirmation avant de mettre en favoris</string>
|
||||||
<string name="drafts_post_reply_removed">Le message auquel répondait ce brouillon a été supprimé</string>
|
<string name="drafts_post_reply_removed">Le message auquel répondait ce brouillon a été supprimé</string>
|
||||||
<string name="drafts_post_failed_to_send">Échec d’envoi du pouet !</string>
|
<string name="drafts_post_failed_to_send">Échec d’envoi du message !</string>
|
||||||
<string name="follow_requests_info">Bien que votre compte ne soit pas verrouillé, l’équipe de %1$s a pensé que vous voudriez valider manuellement les demandes de d’abonnement provenant de ces comptes.</string>
|
<string name="follow_requests_info">Bien que votre compte ne soit pas verrouillé, l’équipe de %1$s a pensé que vous voudriez valider manuellement les demandes de d’abonnement provenant de ces comptes.</string>
|
||||||
<string name="drafts_failed_loading_reply">Échec du chargement des informations de réponse</string>
|
<string name="drafts_failed_loading_reply">Échec du chargement des informations de réponse</string>
|
||||||
<string name="duration_30_days">30 jours</string>
|
<string name="duration_30_days">30 jours</string>
|
||||||
|
@ -540,11 +536,23 @@
|
||||||
<string name="tusky_compose_post_quicksetting_label">Rédiger un message</string>
|
<string name="tusky_compose_post_quicksetting_label">Rédiger un message</string>
|
||||||
<string name="notification_sign_up_format">%s a créé un compte</string>
|
<string name="notification_sign_up_format">%s a créé un compte</string>
|
||||||
<string name="notification_sign_up_name">Nouveaux comptes</string>
|
<string name="notification_sign_up_name">Nouveaux comptes</string>
|
||||||
<string name="notification_sign_up_description">Notifications quand quelqu\'un crée un nouveau compte</string>
|
<string name="notification_sign_up_description">Notifications quand quelqu’un crée un nouveau compte</string>
|
||||||
<string name="pref_title_notification_filter_sign_ups">un nouveau compte a été créé</string>
|
<string name="pref_title_notification_filter_sign_ups">un nouveau compte a été créé</string>
|
||||||
<string name="notification_update_format">%s a modifié son message</string>
|
<string name="notification_update_format">%s a modifié son message</string>
|
||||||
<string name="pref_title_notification_filter_updates">un message avec lequel j\'ai interagi est modifié</string>
|
<string name="pref_title_notification_filter_updates">un message avec lequel j’ai interagi est modifié</string>
|
||||||
<string name="notification_update_name">Messages modifiés</string>
|
<string name="notification_update_name">Messages modifiés</string>
|
||||||
<string name="notification_update_description">Notifications quand un post avec lequel vous avez interagi est modifié</string>
|
<string name="notification_update_description">Notifications quand un message avec lequel vous avez interagi est modifié</string>
|
||||||
<string name="title_login">Se connecter</string>
|
<string name="title_login">Se connecter</string>
|
||||||
|
<string name="account_date_joined">Ici depuis %1$s</string>
|
||||||
|
<string name="action_details">Détails</string>
|
||||||
|
<string name="saving_draft">Sauvegarde du brouillon …</string>
|
||||||
|
<string name="status_count_one_plus">>1</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Tusky peut maintenant recevoir les notifications instantanées de ce compte. Cependant, d\'autres de vos comptes n\'ont pas encore accès aux notifications instantanées. Basculez sur chacun de vos comptes et reconnectez les afin de recevoir les notifications avec UnifiedPush.</string>
|
||||||
|
<string name="error_could_not_load_login_page">La page de connexion ne peut être chargée.</string>
|
||||||
|
<string name="action_edit_image">Retoucher l’image</string>
|
||||||
|
<string name="action_dismiss">Fermer</string>
|
||||||
|
<string name="title_migration_relogin">Se reconnecter pour recevoir les notifications instantanées</string>
|
||||||
|
<string name="dialog_push_notification_migration">Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s’inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l’option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés.</string>
|
||||||
|
<string name="tips_push_notification_migration">Reconnectez tous vos comptes pour activer les notifications instantanées.</string>
|
||||||
|
<string name="error_image_edit_failed">L\'image n’a pas pu être retouchée.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -556,4 +556,11 @@
|
||||||
<string name="title_login">Clàraich a-steach</string>
|
<string name="title_login">Clàraich a-steach</string>
|
||||||
<string name="error_could_not_load_login_page">Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh.</string>
|
<string name="error_could_not_load_login_page">Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh.</string>
|
||||||
<string name="saving_draft">A’ sàbhaladh na dreuchd…</string>
|
<string name="saving_draft">A’ sàbhaladh na dreuchd…</string>
|
||||||
|
<string name="action_dismiss">Leig seachad</string>
|
||||||
|
<string name="action_details">Fiosrachadh</string>
|
||||||
|
<string name="account_date_joined">Air ballrachd fhaighinn %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Clàraich a-steach às ùr leis a h-uile cunntas a chur na taice ri brathan putaidh an comas.</string>
|
||||||
|
<string name="title_migration_relogin">Clàraich a-steach às ùr airson brathan putaidh</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="action_edit_image">Deasaich an dealbh</string>
|
||||||
</resources>
|
</resources>
|
|
@ -52,8 +52,8 @@
|
||||||
<string name="action_block">Bloquear</string>
|
<string name="action_block">Bloquear</string>
|
||||||
<string name="action_unfollow">Deixar de seguir</string>
|
<string name="action_unfollow">Deixar de seguir</string>
|
||||||
<string name="action_follow">Seguir</string>
|
<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_confirm">Tes a certeza de que queres pechar sesión da conta %1$s\?</string>
|
||||||
<string name="action_logout">Desconectar</string>
|
<string name="action_logout">Pechar sesión</string>
|
||||||
<string name="action_login">Accede con Mastodon</string>
|
<string name="action_login">Accede con Mastodon</string>
|
||||||
<string name="action_compose">Redactar</string>
|
<string name="action_compose">Redactar</string>
|
||||||
<string name="action_more">Máis</string>
|
<string name="action_more">Máis</string>
|
||||||
|
@ -521,4 +521,20 @@
|
||||||
<string name="pref_title_notification_filter_sign_ups">hai unha nova usuaria</string>
|
<string name="pref_title_notification_filter_sign_ups">hai unha nova usuaria</string>
|
||||||
<string name="notification_sign_up_name">Rexistros</string>
|
<string name="notification_sign_up_name">Rexistros</string>
|
||||||
<string name="notification_sign_up_description">Notificacións sobre novas usuarias</string>
|
<string name="notification_sign_up_description">Notificacións sobre novas usuarias</string>
|
||||||
|
<string name="pref_title_notification_filter_updates">Foi editada unha publicación coa que interactuei</string>
|
||||||
|
<string name="notification_update_name">Edicións da publicación</string>
|
||||||
|
<string name="account_date_joined">Creada %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Volve a acceder con tódalas contas para activar as notificacións push.</string>
|
||||||
|
<string name="title_login">Acceder</string>
|
||||||
|
<string name="notification_update_description">Notificacións cando son editadas publicacións coas que interactuaches</string>
|
||||||
|
<string name="dialog_push_notification_migration">Para poder usar as notificacións push vía UnifiedPush, Tusky require o permiso para subscribirse ás notificacións do teu servidor Mastodon. É necesario volver a acceder para cambiar os ámbitos OAuth concedidos a Tusky. Usando aquí ou nas Preferencias da Conta a opción de volver a acceder conservarás os borradores locais e caché.</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Volveches a acceder para obter as notificacións push en Tusky. Aínda así tes algunha outra conta que non foi migrada a este modo. Cambia a esas contas e volve a conectar unha a unha para activar o soporte para notificacións de UnifiedPush.</string>
|
||||||
|
<string name="title_migration_relogin">Volve a acceder para ter notificacións push</string>
|
||||||
|
<string name="notification_update_format">%s editou a publicación</string>
|
||||||
|
<string name="action_dismiss">Desbotar</string>
|
||||||
|
<string name="action_details">Detalles</string>
|
||||||
|
<string name="error_could_not_load_login_page">Non se puido cargar a páxina de inicio.</string>
|
||||||
|
<string name="saving_draft">Gardando borrador…</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="action_edit_image">Editar imaxe</string>
|
||||||
</resources>
|
</resources>
|
|
@ -533,4 +533,18 @@
|
||||||
<string name="notification_update_name">Bejegyzések szerkesztése</string>
|
<string name="notification_update_name">Bejegyzések szerkesztése</string>
|
||||||
<string name="notification_update_description">Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt</string>
|
<string name="notification_update_description">Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt</string>
|
||||||
<string name="tusky_compose_post_quicksetting_label">Bejegyzés Létrehozása</string>
|
<string name="tusky_compose_post_quicksetting_label">Bejegyzés Létrehozása</string>
|
||||||
|
<string name="title_migration_relogin">Bejelentkezés újra a leküldési értesítések érdekében</string>
|
||||||
|
<string name="action_dismiss">Elvetés</string>
|
||||||
|
<string name="action_details">Részletek</string>
|
||||||
|
<string name="account_date_joined">Csatlakozva %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Bejelentkezés újra minden fiókkal a leküldéses értesítések engedélyezése érdekében.</string>
|
||||||
|
<string name="title_login">Bejelentkezés</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="error_could_not_load_login_page">Nem tudtuk betölteni a bejelentkező oldalt.</string>
|
||||||
|
<string name="saving_draft">Vázlat mentése…</string>
|
||||||
|
<string name="dialog_push_notification_migration">Ahhoz, hogy használhass leküldési értesítéseket a UnifiedPush szolgáltatással, a Tusky-nak fel kell iratkoznia az értesítésekre a Mastodon szervereden. Ehhez új bejelentkezésre van szükség, hogy a Tusky számára kiosztott OAuth jogosultságok megváltozzanak. Az újbóli bejelentkezés funkció használata itt vagy a Fiókbeállításoknál meg fogja őrizni a helyi piszkozataidat és a cache tartalmát.</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tusky-t a leküldési értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így migrálva. Válts át rájuk és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását.</string>
|
||||||
|
<string name="action_edit_image">Kép szerkesztése</string>
|
||||||
|
<string name="error_image_edit_failed">A kép nem szerkeszthető.</string>
|
||||||
|
<string name="error_loading_account_details">Nem sikerült betölteni a fiókadatokat</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -525,4 +525,16 @@
|
||||||
<string name="notification_sign_up_description">Tilkynningar um nýja notendur</string>
|
<string name="notification_sign_up_description">Tilkynningar um nýja notendur</string>
|
||||||
<string name="notification_update_name">Breytingar á færslum</string>
|
<string name="notification_update_name">Breytingar á færslum</string>
|
||||||
<string name="notification_update_description">Tilkynningar þegar færslum sem þú hefur átt við er breytt</string>
|
<string name="notification_update_description">Tilkynningar þegar færslum sem þú hefur átt við er breytt</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Þú hefur skráð þig aftur inn í fyrirliggjandi aðganginn þinn til þess að veita heimild fyrir áskrift að ýti-tilkynningum í Tusky. Aftur á móti ertu með aðra aðganga sem ekki hafa verið yfirfærðir á þennan hátt. Skiptu yfir í þá og skráðu þig þar inn aftur til að virkja stuðning við tilkynningar í gegnum UnifiedPush.</string>
|
||||||
|
<string name="account_date_joined">Skráði sig %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Skrá aftur inn alla aðganga til að virkja stuðning við ýti-tilkynningar.</string>
|
||||||
|
<string name="dialog_push_notification_migration">Til þess að geta sent ýti-tilkynningar í gegnum UnifiedPush, þarf Tusky heimild til að gerast áskrifandi að tilkynningum á Mastodon-netþjóninum þínum. Þetta krefst þess að skráð sé inn aftur til að breyta vægi OAuth-heimilda sem Tusky er úthlutað. Notaðu endurinnskráninguna hérna eða í kjörstillingum aðgangsins þíns til að varðveita öll drögin þín og skyndiminni á tækinu.</string>
|
||||||
|
<string name="title_login">Skrá inn</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="error_could_not_load_login_page">Gat ekki lesið innskráningarsíðuna.</string>
|
||||||
|
<string name="action_edit_image">Breyta mynd</string>
|
||||||
|
<string name="saving_draft">Vista drög…</string>
|
||||||
|
<string name="title_migration_relogin">Skráðu aftur inn fyrir ýti-tilkynningar</string>
|
||||||
|
<string name="action_dismiss">Hunsa</string>
|
||||||
|
<string name="action_details">Nánar</string>
|
||||||
</resources>
|
</resources>
|
|
@ -30,7 +30,7 @@
|
||||||
<string name="title_posts_with_replies">Con risposte</string>
|
<string name="title_posts_with_replies">Con risposte</string>
|
||||||
<string name="title_posts_pinned">Fissati</string>
|
<string name="title_posts_pinned">Fissati</string>
|
||||||
<string name="title_follows">Seguiti</string>
|
<string name="title_follows">Seguiti</string>
|
||||||
<string name="title_followers">Seguono</string>
|
<string name="title_followers">Seguaci</string>
|
||||||
<string name="title_favourites">Preferiti</string>
|
<string name="title_favourites">Preferiti</string>
|
||||||
<string name="title_mutes">Utenti silenziati</string>
|
<string name="title_mutes">Utenti silenziati</string>
|
||||||
<string name="title_blocks">Utenti bloccati</string>
|
<string name="title_blocks">Utenti bloccati</string>
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
<string name="title_drafts">Bozze</string>
|
<string name="title_drafts">Bozze</string>
|
||||||
<string name="title_licenses">Licenze</string>
|
<string name="title_licenses">Licenze</string>
|
||||||
<string name="post_username_format">\@%s</string>
|
<string name="post_username_format">\@%s</string>
|
||||||
<string name="post_boosted_format">%s ha boostato</string>
|
<string name="post_boosted_format">%s ha condiviso</string>
|
||||||
<string name="post_sensitive_media_title">Contenuto sensibile</string>
|
<string name="post_sensitive_media_title">Contenuto sensibile</string>
|
||||||
<string name="post_media_hidden_title">Media nascosto</string>
|
<string name="post_media_hidden_title">Media nascosto</string>
|
||||||
<string name="post_sensitive_media_directions">Clicca per visualizzare</string>
|
<string name="post_sensitive_media_directions">Clicca per visualizzare</string>
|
||||||
|
@ -49,15 +49,15 @@
|
||||||
<string name="post_content_show_less">Riduci</string>
|
<string name="post_content_show_less">Riduci</string>
|
||||||
<string name="message_empty">Qui non c\'è nulla.</string>
|
<string name="message_empty">Qui non c\'è nulla.</string>
|
||||||
<string name="footer_empty">Qui non c\'è nulla. Trascina verso il basso per aggiornare!</string>
|
<string name="footer_empty">Qui non c\'è nulla. Trascina verso il basso per aggiornare!</string>
|
||||||
<string name="notification_reblog_format">%s ha boostato il tuo post</string>
|
<string name="notification_reblog_format">%s ha condiviso il tuo post</string>
|
||||||
<string name="notification_favourite_format">%s ha messo il tuo post nei preferiti</string>
|
<string name="notification_favourite_format">%s ha messo il tuo post nei preferiti</string>
|
||||||
<string name="notification_follow_format">%s ti ha seguito</string>
|
<string name="notification_follow_format">%s ti ha seguito</string>
|
||||||
<string name="report_username_format">Segnala @%s</string>
|
<string name="report_username_format">Segnala @%s</string>
|
||||||
<string name="report_comment_hint">Commenti aggiuntivi?</string>
|
<string name="report_comment_hint">Commenti aggiuntivi?</string>
|
||||||
<string name="action_quick_reply">Risposta veloce</string>
|
<string name="action_quick_reply">Risposta veloce</string>
|
||||||
<string name="action_reply">Rispondi</string>
|
<string name="action_reply">Rispondi</string>
|
||||||
<string name="action_reblog">Boosta</string>
|
<string name="action_reblog">Condividi</string>
|
||||||
<string name="action_unreblog">Rimuovi boost</string>
|
<string name="action_unreblog">Rimuovi condivisione</string>
|
||||||
<string name="action_favourite">Aggiungi ai preferiti</string>
|
<string name="action_favourite">Aggiungi ai preferiti</string>
|
||||||
<string name="action_unfavourite">Rimuovi preferito</string>
|
<string name="action_unfavourite">Rimuovi preferito</string>
|
||||||
<string name="action_more">Di più</string>
|
<string name="action_more">Di più</string>
|
||||||
|
@ -69,8 +69,8 @@
|
||||||
<string name="action_unfollow">Smetti di seguire</string>
|
<string name="action_unfollow">Smetti di seguire</string>
|
||||||
<string name="action_block">Blocca</string>
|
<string name="action_block">Blocca</string>
|
||||||
<string name="action_unblock">Sblocca</string>
|
<string name="action_unblock">Sblocca</string>
|
||||||
<string name="action_hide_reblogs">Nascondi boost</string>
|
<string name="action_hide_reblogs">Nascondi condivisioni</string>
|
||||||
<string name="action_show_reblogs">Mostra boost</string>
|
<string name="action_show_reblogs">Mostra condivisioni</string>
|
||||||
<string name="action_report">Segnala</string>
|
<string name="action_report">Segnala</string>
|
||||||
<string name="action_delete">Elimina</string>
|
<string name="action_delete">Elimina</string>
|
||||||
<string name="action_send">TOOT</string>
|
<string name="action_send">TOOT</string>
|
||||||
|
@ -109,8 +109,8 @@
|
||||||
<string name="action_links">Collegamenti</string>
|
<string name="action_links">Collegamenti</string>
|
||||||
<string name="action_mentions">Menzioni</string>
|
<string name="action_mentions">Menzioni</string>
|
||||||
<string name="action_hashtags">Hashtag</string>
|
<string name="action_hashtags">Hashtag</string>
|
||||||
<string name="action_open_reblogger">Vai all\'autore del boost</string>
|
<string name="action_open_reblogger">Vai all\'autore della condivisione</string>
|
||||||
<string name="action_open_reblogged_by">Mostra boost</string>
|
<string name="action_open_reblogged_by">Mostra condivisioni</string>
|
||||||
<string name="action_open_faved_by">Mostra preferiti</string>
|
<string name="action_open_faved_by">Mostra preferiti</string>
|
||||||
<string name="title_hashtags_dialog">Hashtag</string>
|
<string name="title_hashtags_dialog">Hashtag</string>
|
||||||
<string name="title_mentions_dialog">Menzioni</string>
|
<string name="title_mentions_dialog">Menzioni</string>
|
||||||
|
@ -153,8 +153,8 @@
|
||||||
<string name="dialog_message_cancel_follow_request">Revocare la richiesta di seguire?</string>
|
<string name="dialog_message_cancel_follow_request">Revocare la richiesta di seguire?</string>
|
||||||
<string name="dialog_unfollow_warning">Smettere di seguire questo account?</string>
|
<string name="dialog_unfollow_warning">Smettere di seguire questo account?</string>
|
||||||
<string name="dialog_delete_post_warning">Eliminare questo post\?</string>
|
<string name="dialog_delete_post_warning">Eliminare questo post\?</string>
|
||||||
<string name="visibility_public">Pubblico: visibile sulla timeline pubblica</string>
|
<string name="visibility_public">Pubblico: visibile sulle timeline pubbliche</string>
|
||||||
<string name="visibility_unlisted">Non in elenco: non visibile sulla timeline pubblica e locale</string>
|
<string name="visibility_unlisted">Non in elenco: non visibile sulle timeline pubbliche</string>
|
||||||
<string name="visibility_private">Solo follower: visibile solo dai tuoi follower</string>
|
<string name="visibility_private">Solo follower: visibile solo dai tuoi follower</string>
|
||||||
<string name="visibility_direct">Diretto: visibile solo agli utenti menzionati</string>
|
<string name="visibility_direct">Diretto: visibile solo agli utenti menzionati</string>
|
||||||
<string name="pref_title_edit_notification_settings">Notifiche</string>
|
<string name="pref_title_edit_notification_settings">Notifiche</string>
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
<string name="pref_title_notification_filters">Notificami quando</string>
|
<string name="pref_title_notification_filters">Notificami quando</string>
|
||||||
<string name="pref_title_notification_filter_mentions">vengo menzionato</string>
|
<string name="pref_title_notification_filter_mentions">vengo menzionato</string>
|
||||||
<string name="pref_title_notification_filter_follows">vengo seguito</string>
|
<string name="pref_title_notification_filter_follows">vengo seguito</string>
|
||||||
<string name="pref_title_notification_filter_reblogs">i miei post vengono boostati</string>
|
<string name="pref_title_notification_filter_reblogs">i miei post vengono condivisi</string>
|
||||||
<string name="pref_title_notification_filter_favourites">i miei post vengono messi nei preferiti</string>
|
<string name="pref_title_notification_filter_favourites">i miei post vengono messi nei preferiti</string>
|
||||||
<string name="pref_title_appearance_settings">Aspetto</string>
|
<string name="pref_title_appearance_settings">Aspetto</string>
|
||||||
<string name="pref_title_app_theme">Tema dell\'app</string>
|
<string name="pref_title_app_theme">Tema dell\'app</string>
|
||||||
|
@ -183,7 +183,7 @@
|
||||||
<string name="pref_title_language">Lingua</string>
|
<string name="pref_title_language">Lingua</string>
|
||||||
<string name="pref_title_post_filter">Filtraggio della timeline</string>
|
<string name="pref_title_post_filter">Filtraggio della timeline</string>
|
||||||
<string name="pref_title_post_tabs">Schede</string>
|
<string name="pref_title_post_tabs">Schede</string>
|
||||||
<string name="pref_title_show_boosts">Mostra boost</string>
|
<string name="pref_title_show_boosts">Mostra condivisioni</string>
|
||||||
<string name="pref_title_show_replies">Mostra risposte</string>
|
<string name="pref_title_show_replies">Mostra risposte</string>
|
||||||
<string name="pref_title_show_media_preview">Mostra anteprime media</string>
|
<string name="pref_title_show_media_preview">Mostra anteprime media</string>
|
||||||
<string name="pref_title_proxy_settings">Proxy</string>
|
<string name="pref_title_proxy_settings">Proxy</string>
|
||||||
|
@ -208,8 +208,8 @@
|
||||||
<string name="notification_mention_descriptions">Notifiche di quando vieni menzionato da qualcuno</string>
|
<string name="notification_mention_descriptions">Notifiche di quando vieni menzionato da qualcuno</string>
|
||||||
<string name="notification_follow_name">Nuovi follower</string>
|
<string name="notification_follow_name">Nuovi follower</string>
|
||||||
<string name="notification_follow_description">Notifiche su nuovi follower</string>
|
<string name="notification_follow_description">Notifiche su nuovi follower</string>
|
||||||
<string name="notification_boost_name">Boost</string>
|
<string name="notification_boost_name">Condivisioni</string>
|
||||||
<string name="notification_boost_description">Notifiche sui tuoi post che vengono boostati</string>
|
<string name="notification_boost_description">Notifiche sui tuoi post che vengono condivisi</string>
|
||||||
<string name="notification_favourite_name">Preferiti</string>
|
<string name="notification_favourite_name">Preferiti</string>
|
||||||
<string name="notification_favourite_description">Notifiche sui tuoi post che vengono segnati come preferiti</string>
|
<string name="notification_favourite_description">Notifiche sui tuoi post che vengono segnati come preferiti</string>
|
||||||
<string name="notification_mention_format">%s ti ha menzionato</string>
|
<string name="notification_mention_format">%s ti ha menzionato</string>
|
||||||
|
@ -312,8 +312,8 @@
|
||||||
<string name="download_failed">Download fallito</string>
|
<string name="download_failed">Download fallito</string>
|
||||||
<string name="profile_badge_bot_text">Bot</string>
|
<string name="profile_badge_bot_text">Bot</string>
|
||||||
<string name="account_moved_description">%1$s si è spostato su:</string>
|
<string name="account_moved_description">%1$s si è spostato su:</string>
|
||||||
<string name="reblog_private">Boost con la visibilità del post di origine</string>
|
<string name="reblog_private">Condividi con la visibilità del post originale</string>
|
||||||
<string name="unreblog_private">Annulla boost</string>
|
<string name="unreblog_private">Annulla condivisione</string>
|
||||||
<string name="license_description">Yuito contiene codice e risorse dai seguenti progetti open source:</string>
|
<string name="license_description">Yuito contiene codice e risorse dai seguenti progetti open source:</string>
|
||||||
<string name="license_apache_2">Licenziata sotto la Licenza Apache (copia sotto)</string>
|
<string name="license_apache_2">Licenziata sotto la Licenza Apache (copia sotto)</string>
|
||||||
<string name="license_cc_by_4">CC-BY 4.0</string>
|
<string name="license_cc_by_4">CC-BY 4.0</string>
|
||||||
|
@ -334,7 +334,7 @@
|
||||||
<item quantity="one"><b>%s</b> Boost</item>
|
<item quantity="one"><b>%s</b> Boost</item>
|
||||||
<item quantity="other"><b>%s</b> Boost</item>
|
<item quantity="other"><b>%s</b> Boost</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="title_reblogged_by">Boostato da</string>
|
<string name="title_reblogged_by">Condiviso da</string>
|
||||||
<string name="title_favourited_by">Aggiunto ai preferiti da</string>
|
<string name="title_favourited_by">Aggiunto ai preferiti da</string>
|
||||||
<string name="conversation_1_recipients">%1$s</string>
|
<string name="conversation_1_recipients">%1$s</string>
|
||||||
<string name="conversation_2_recipients">%1$s e %2$s</string>
|
<string name="conversation_2_recipients">%1$s e %2$s</string>
|
||||||
|
@ -402,7 +402,7 @@
|
||||||
<string name="select_list_title">Scegli lista</string>
|
<string name="select_list_title">Scegli lista</string>
|
||||||
<string name="list">Lista</string>
|
<string name="list">Lista</string>
|
||||||
<string name="compose_preview_image_description">Azioni per l\'immagine %s</string>
|
<string name="compose_preview_image_description">Azioni per l\'immagine %s</string>
|
||||||
<string name="poll_ended_voted">Un sondaggio che hai votato si è concluso</string>
|
<string name="poll_ended_voted">Un sondaggio in cui hai votato si è concluso</string>
|
||||||
<string name="poll_ended_created">Un sondaggio che hai creato si è concluso</string>
|
<string name="poll_ended_created">Un sondaggio che hai creato si è concluso</string>
|
||||||
<plurals name="poll_timespan_days">
|
<plurals name="poll_timespan_days">
|
||||||
<item quantity="one">%d giorno rimasto</item>
|
<item quantity="one">%d giorno rimasto</item>
|
||||||
|
@ -470,7 +470,7 @@
|
||||||
<string name="account_note_saved">Salvato!</string>
|
<string name="account_note_saved">Salvato!</string>
|
||||||
<string name="account_note_hint">La tua nota privata su questo account</string>
|
<string name="account_note_hint">La tua nota privata su questo account</string>
|
||||||
<string name="pref_title_hide_top_toolbar">Nascondi il titolo della barra degli strumenti in alto</string>
|
<string name="pref_title_hide_top_toolbar">Nascondi il titolo della barra degli strumenti in alto</string>
|
||||||
<string name="pref_title_confirm_reblogs">Mostra la finestra di conferma prima di boostare</string>
|
<string name="pref_title_confirm_reblogs">Mostra la finestra di conferma prima di condividere</string>
|
||||||
<string name="pref_title_show_cards_in_timelines">Mostra le anteprime dei collegamenti nelle timelines</string>
|
<string name="pref_title_show_cards_in_timelines">Mostra le anteprime dei collegamenti nelle timelines</string>
|
||||||
<string name="warning_scheduling_interval">Mastodon ha un intervallo di programmazione minimo di 5 minuti.</string>
|
<string name="warning_scheduling_interval">Mastodon ha un intervallo di programmazione minimo di 5 minuti.</string>
|
||||||
<string name="no_announcements">Non ci sono annunci.</string>
|
<string name="no_announcements">Non ci sono annunci.</string>
|
||||||
|
@ -488,7 +488,7 @@
|
||||||
<string name="pref_title_notification_filter_follow_requests">mi viene richiesto di seguirmi</string>
|
<string name="pref_title_notification_filter_follow_requests">mi viene richiesto di seguirmi</string>
|
||||||
<string name="wellbeing_hide_stats_profile">Nascondi statistiche quantitative sui profili</string>
|
<string name="wellbeing_hide_stats_profile">Nascondi statistiche quantitative sui profili</string>
|
||||||
<string name="wellbeing_hide_stats_posts">Nascondi le statistiche quantitative sui post</string>
|
<string name="wellbeing_hide_stats_posts">Nascondi le statistiche quantitative sui post</string>
|
||||||
<string name="limit_notifications">Limita le notifiche dalla timeline</string>
|
<string name="limit_notifications">Limita notifiche riguardo statistiche quantitative</string>
|
||||||
<string name="review_notifications">Rivedi le notifiche</string>
|
<string name="review_notifications">Rivedi le notifiche</string>
|
||||||
<string name="pref_title_wellbeing_mode">Benessere</string>
|
<string name="pref_title_wellbeing_mode">Benessere</string>
|
||||||
<string name="notification_subscription_description">Notifiche di nuovi post di qualcuno a cui sei iscritto</string>
|
<string name="notification_subscription_description">Notifiche di nuovi post di qualcuno a cui sei iscritto</string>
|
||||||
|
@ -515,13 +515,13 @@
|
||||||
<string name="action_delete_conversation">Elimina conversazione</string>
|
<string name="action_delete_conversation">Elimina conversazione</string>
|
||||||
<string name="wellbeing_mode_notice">Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include:
|
<string name="wellbeing_mode_notice">Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include:
|
||||||
\n
|
\n
|
||||||
\n - Notifiche riguardo a Preferiti/Boost/Following
|
\n - Notifiche riguardo a Preferiti/Condivisioni/Following
|
||||||
\n - Conteggio dei Preferiti/Boost nei post
|
\n - Conteggio dei Preferiti/Condivisioni nei post
|
||||||
\n - Statistiche riguardo a Preferiti/Post nei profili
|
\n - Statistiche riguardo a Preferiti/Post nei profili
|
||||||
\n
|
\n
|
||||||
\n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente.</string>
|
\n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente.</string>
|
||||||
<string name="action_unbookmark">Rimuovi segnalibro</string>
|
<string name="action_unbookmark">Rimuovi segnalibro</string>
|
||||||
<string name="pref_title_confirm_favourites">Chiedi conferma prima di boostare</string>
|
<string name="pref_title_confirm_favourites">Chiedi conferma prima di condividere</string>
|
||||||
<string name="duration_14_days">14 giorni</string>
|
<string name="duration_14_days">14 giorni</string>
|
||||||
<string name="duration_30_days">30 giorni</string>
|
<string name="duration_30_days">30 giorni</string>
|
||||||
<string name="duration_60_days">60 giorni</string>
|
<string name="duration_60_days">60 giorni</string>
|
||||||
|
@ -540,4 +540,8 @@
|
||||||
<string name="notification_update_name">Modifiche ai post</string>
|
<string name="notification_update_name">Modifiche ai post</string>
|
||||||
<string name="notification_update_description">Notifiche di quando i post con cui hai interagito vengono modificati</string>
|
<string name="notification_update_description">Notifiche di quando i post con cui hai interagito vengono modificati</string>
|
||||||
<string name="error_could_not_load_login_page">Non è stato possibile caricare la pagina di login.</string>
|
<string name="error_could_not_load_login_page">Non è stato possibile caricare la pagina di login.</string>
|
||||||
|
<string name="action_edit_image">Modifica immagine</string>
|
||||||
|
<string name="saving_draft">Salvataggio bozza…</string>
|
||||||
|
<string name="action_dismiss">Scartare</string>
|
||||||
|
<string name="action_details">Dettagli</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -527,4 +527,16 @@
|
||||||
<string name="notification_update_description">Varslinger når et innlegg du har hatt en interaksjon med er redigert</string>
|
<string name="notification_update_description">Varslinger når et innlegg du har hatt en interaksjon med er redigert</string>
|
||||||
<string name="title_login">Innlogging</string>
|
<string name="title_login">Innlogging</string>
|
||||||
<string name="error_could_not_load_login_page">Klarte ikke å laste innloggingssiden.</string>
|
<string name="error_could_not_load_login_page">Klarte ikke å laste innloggingssiden.</string>
|
||||||
|
<string name="title_migration_relogin">Logg inn på nytt for pushvarsler</string>
|
||||||
|
<string name="action_dismiss">Avvis</string>
|
||||||
|
<string name="action_details">Detaljer</string>
|
||||||
|
<string name="account_date_joined">Ble med %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Logg inn all konti på nytt for å skru på pushvarsler.</string>
|
||||||
|
<string name="dialog_push_notification_migration">For å kunne sende pushvarsler via UnifiedPush trenger Tusky tillatelse til å abonnere på varsler på Mastodon-serveren. Dette krever at du logger inn på nytt. Ved å bruke muligheten til å logge inn på nytt her eller i kontoinstillinger vil alle lokale kladder være tilgjengelig også etter at du har logget inn på nytt.</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Du har logget inn på nytt for å tillate Tusky til å sende pushvarsler, men du har fortsatt andre konti som ikke har fått den nødvendige tillatelsen. Bytt til dem og logg inn på nytt på samme måte for å skru på støtte for pushvarsler via UnifiedPush.</string>
|
||||||
|
<string name="saving_draft">Lagrer kladd…</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="action_edit_image">Rediger bilde</string>
|
||||||
|
<string name="error_image_edit_failed">Bildet kunne ikke redigeres.</string>
|
||||||
|
<string name="error_loading_account_details">Lasting av kontodetaljer feilet</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -65,10 +65,10 @@
|
||||||
<string name="title_announcements">Anúncios</string>
|
<string name="title_announcements">Anúncios</string>
|
||||||
<string name="title_licenses">Licenças</string>
|
<string name="title_licenses">Licenças</string>
|
||||||
<string name="post_username_format">\@%s</string>
|
<string name="post_username_format">\@%s</string>
|
||||||
<string name="post_boosted_format">%s fez boost</string>
|
<string name="post_boosted_format">%s deu boost</string>
|
||||||
<string name="message_empty">Nada aqui.</string>
|
<string name="message_empty">Nada aqui.</string>
|
||||||
<string name="footer_empty">Nada para ver aqui. Arraste para baixo para atualizar!</string>
|
<string name="footer_empty">Nada para ver aqui. Arraste para baixo para atualizar!</string>
|
||||||
<string name="notification_reblog_format">%s fez boost ao seu toot</string>
|
<string name="notification_reblog_format">%s deu boost ao seu toot</string>
|
||||||
<string name="notification_favourite_format">%s adicionou o seu toot aos favoritos</string>
|
<string name="notification_favourite_format">%s adicionou o seu toot aos favoritos</string>
|
||||||
<string name="notification_follow_format">%s está a seguir-te</string>
|
<string name="notification_follow_format">%s está a seguir-te</string>
|
||||||
<string name="notification_follow_request_format">%s pediu para te seguir</string>
|
<string name="notification_follow_request_format">%s pediu para te seguir</string>
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
<string name="action_open_drawer">Abrir menu</string>
|
<string name="action_open_drawer">Abrir menu</string>
|
||||||
<string name="action_search">Pesquisar</string>
|
<string name="action_search">Pesquisar</string>
|
||||||
<string name="action_access_drafts">Rascunhos</string>
|
<string name="action_access_drafts">Rascunhos</string>
|
||||||
<string name="action_access_scheduled_posts">Toots agendados</string>
|
<string name="action_access_scheduled_posts">Toots Agendados</string>
|
||||||
<string name="action_toggle_visibility">Privacidade do toot</string>
|
<string name="action_toggle_visibility">Privacidade do toot</string>
|
||||||
<string name="action_content_warning">Aviso de conteúdo</string>
|
<string name="action_content_warning">Aviso de conteúdo</string>
|
||||||
<string name="action_emoji_keyboard">Teclado de emojis</string>
|
<string name="action_emoji_keyboard">Teclado de emojis</string>
|
||||||
|
@ -325,7 +325,6 @@
|
||||||
<string name="action_lists">Listas</string>
|
<string name="action_lists">Listas</string>
|
||||||
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
||||||
<string name="title_lists">Listas</string>
|
<string name="title_lists">Listas</string>
|
||||||
|
|
||||||
<string name="error_create_list">Não foi possível criar a lista</string>
|
<string name="error_create_list">Não foi possível criar a lista</string>
|
||||||
<string name="error_delete_list">Não foi possível apagar a lista</string>
|
<string name="error_delete_list">Não foi possível apagar a lista</string>
|
||||||
<string name="action_create_list">Criar uma lista</string>
|
<string name="action_create_list">Criar uma lista</string>
|
||||||
|
|
|
@ -550,4 +550,15 @@
|
||||||
<string name="title_login">Вхід</string>
|
<string name="title_login">Вхід</string>
|
||||||
<string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string>
|
<string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string>
|
||||||
<string name="saving_draft">Збереження чернетки…</string>
|
<string name="saving_draft">Збереження чернетки…</string>
|
||||||
|
<string name="action_dismiss">Відхилити</string>
|
||||||
|
<string name="action_details">Подробиці</string>
|
||||||
|
<string name="title_migration_relogin">Увійдіть повторно, щоб отримувати push-сповіщення</string>
|
||||||
|
<string name="tips_push_notification_migration">Увійдіть повторно до всіх облікових записів, щоб увімкнути підтримку push-сповіщень.</string>
|
||||||
|
<string name="dialog_push_notification_migration">Щоб використовувати push-сповіщення через UnifiedPush, Tusky потребує дозволу стежити за сповіщеннями на вашому сервері Mastodon. Це вимагає повторного входу, щоб змінити області OAuth, надані Tusky. Використання параметра повторного входу тут або в налаштуваннях облікового запису збереже всі ваші локальні чернетки та кеш.</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Ви повторно увійшли до свого поточного облікового запису, щоб надати дозвіл на стеження Tusky. Однак у вас все ще є інші облікові записи, які не мігрували таким чином. Перейдіть до них і повторно увійдіть до них по одному, щоб забезпечити підтримку UnifiedPush сповіщень.</string>
|
||||||
|
<string name="account_date_joined">Приєднується %1$s</string>
|
||||||
|
<string name="action_edit_image">Редагувати зображення</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="error_image_edit_failed">Неможливо редагувати зображення.</string>
|
||||||
|
<string name="error_loading_account_details">Не вдалося завантажити подробиці облікового запису</string>
|
||||||
</resources>
|
</resources>
|
|
@ -517,4 +517,15 @@
|
||||||
<string name="title_login">Đăng nhập</string>
|
<string name="title_login">Đăng nhập</string>
|
||||||
<string name="error_could_not_load_login_page">Không thể tải trang đăng nhập.</string>
|
<string name="error_could_not_load_login_page">Không thể tải trang đăng nhập.</string>
|
||||||
<string name="saving_draft">Đang lưu nháp…</string>
|
<string name="saving_draft">Đang lưu nháp…</string>
|
||||||
|
<string name="action_dismiss">Bỏ qua</string>
|
||||||
|
<string name="title_migration_relogin">Đăng nhập lại để hiện thông báo đẩy</string>
|
||||||
|
<string name="action_details">Chi tiết</string>
|
||||||
|
<string name="account_date_joined">Tham gia vào %1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">Đăng nhập lại tất cả tài khoản để kích hoạt thông báo đẩy.</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">Bạn đã đăng nhập lại vào tài khoản hiện tại của mình để cấp quyền thông báo đẩy cho Tusky. Tuy nhiên, bạn vẫn có các tài khoản khác chưa kích hoạt thông báo đẩy theo cách này. Chuyển sang chúng và đăng nhập từng cái một để cho phép hỗ trợ thông báo UnifiedPush.</string>
|
||||||
|
<string name="dialog_push_notification_migration">Để sử dụng thông báo đẩy qua UnifiedPush, Tusky cần có quyền đăng ký thông báo trên máy chủ Mastodon của bạn. Bạn hãy thoát ra rồi đăng nhập lại để thay đổi phạm vi OAuth được cấp cho Tusky. Sử dụng đăng nhập lại ở đây hoặc trong cài đặt Tài khoản sẽ bảo toàn tất cả các tút nháp và bộ nhớ đệm trên điện thoại của bạn.</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="action_edit_image">Sửa ảnh</string>
|
||||||
|
<string name="error_image_edit_failed">Hình ảnh này không thể sửa.</string>
|
||||||
|
<string name="error_loading_account_details">Không thể tải thông tin tài khoản</string>
|
||||||
</resources>
|
</resources>
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="title_posts_pinned">已置顶</string>
|
<string name="title_posts_pinned">已置顶</string>
|
||||||
<string name="title_follows">正在关注</string>
|
<string name="title_follows">正在关注</string>
|
||||||
<string name="title_followers">关注者</string>
|
<string name="title_followers">关注者</string>
|
||||||
<string name="title_favourites">收藏</string>
|
<string name="title_favourites">喜欢</string>
|
||||||
<string name="title_mutes">被隐藏的用户</string>
|
<string name="title_mutes">被隐藏的用户</string>
|
||||||
<string name="title_blocks">被屏蔽的用户</string>
|
<string name="title_blocks">被屏蔽的用户</string>
|
||||||
<string name="title_follow_requests">关注请求</string>
|
<string name="title_follow_requests">关注请求</string>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<string name="message_empty">还没有内容。</string>
|
<string name="message_empty">还没有内容。</string>
|
||||||
<string name="footer_empty">还没有内容,向下拉动即可刷新!</string>
|
<string name="footer_empty">还没有内容,向下拉动即可刷新!</string>
|
||||||
<string name="notification_reblog_format">%s 转嘟了你的嘟文</string>
|
<string name="notification_reblog_format">%s 转嘟了你的嘟文</string>
|
||||||
<string name="notification_favourite_format">%s 收藏了你的嘟文</string>
|
<string name="notification_favourite_format">%s 喜欢了你的嘟文</string>
|
||||||
<string name="notification_follow_format">%s 关注了你</string>
|
<string name="notification_follow_format">%s 关注了你</string>
|
||||||
<string name="report_username_format">举报 @%s</string>
|
<string name="report_username_format">举报 @%s</string>
|
||||||
<string name="report_comment_hint">是否有更多信息需报告?</string>
|
<string name="report_comment_hint">是否有更多信息需报告?</string>
|
||||||
|
@ -58,8 +58,8 @@
|
||||||
<string name="action_reply">回复</string>
|
<string name="action_reply">回复</string>
|
||||||
<string name="action_reblog">转嘟</string>
|
<string name="action_reblog">转嘟</string>
|
||||||
<string name="action_unreblog">取消转嘟</string>
|
<string name="action_unreblog">取消转嘟</string>
|
||||||
<string name="action_favourite">收藏</string>
|
<string name="action_favourite">喜欢</string>
|
||||||
<string name="action_unfavourite">取消收藏</string>
|
<string name="action_unfavourite">取消喜欢</string>
|
||||||
<string name="action_more">更多</string>
|
<string name="action_more">更多</string>
|
||||||
<string name="action_compose">发表嘟文</string>
|
<string name="action_compose">发表嘟文</string>
|
||||||
<string name="action_login">登录 Mastodon 帐号</string>
|
<string name="action_login">登录 Mastodon 帐号</string>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<string name="action_view_profile">个人资料</string>
|
<string name="action_view_profile">个人资料</string>
|
||||||
<string name="action_view_preferences">设置</string>
|
<string name="action_view_preferences">设置</string>
|
||||||
<string name="action_view_account_preferences">帐户设置</string>
|
<string name="action_view_account_preferences">帐户设置</string>
|
||||||
<string name="action_view_favourites">收藏</string>
|
<string name="action_view_favourites">喜欢</string>
|
||||||
<string name="action_view_mutes">被隐藏的用户</string>
|
<string name="action_view_mutes">被隐藏的用户</string>
|
||||||
<string name="action_view_blocks">被屏蔽的用户</string>
|
<string name="action_view_blocks">被屏蔽的用户</string>
|
||||||
<string name="action_view_follow_requests">关注请求</string>
|
<string name="action_view_follow_requests">关注请求</string>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
<string name="action_hashtags">话题</string>
|
<string name="action_hashtags">话题</string>
|
||||||
<string name="action_open_reblogger">打开转嘟用户主页</string>
|
<string name="action_open_reblogger">打开转嘟用户主页</string>
|
||||||
<string name="action_open_reblogged_by">显示转嘟</string>
|
<string name="action_open_reblogged_by">显示转嘟</string>
|
||||||
<string name="action_open_faved_by">显示收藏</string>
|
<string name="action_open_faved_by">显示喜欢</string>
|
||||||
<string name="title_hashtags_dialog">话题</string>
|
<string name="title_hashtags_dialog">话题</string>
|
||||||
<string name="title_mentions_dialog">提及</string>
|
<string name="title_mentions_dialog">提及</string>
|
||||||
<string name="title_links_dialog">链接</string>
|
<string name="title_links_dialog">链接</string>
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
<string name="pref_title_notification_filter_mentions">被提及</string>
|
<string name="pref_title_notification_filter_mentions">被提及</string>
|
||||||
<string name="pref_title_notification_filter_follows">有新的关注者</string>
|
<string name="pref_title_notification_filter_follows">有新的关注者</string>
|
||||||
<string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string>
|
<string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string>
|
||||||
<string name="pref_title_notification_filter_favourites">嘟文被收藏</string>
|
<string name="pref_title_notification_filter_favourites">嘟文被喜欢</string>
|
||||||
<string name="pref_title_notification_filter_poll">投票已结束</string>
|
<string name="pref_title_notification_filter_poll">投票已结束</string>
|
||||||
<string name="pref_title_appearance_settings">外观</string>
|
<string name="pref_title_appearance_settings">外观</string>
|
||||||
<string name="pref_title_app_theme">应用主题</string>
|
<string name="pref_title_app_theme">应用主题</string>
|
||||||
|
@ -215,8 +215,8 @@
|
||||||
<string name="notification_follow_description">当有用户关注我时</string>
|
<string name="notification_follow_description">当有用户关注我时</string>
|
||||||
<string name="notification_boost_name">转嘟</string>
|
<string name="notification_boost_name">转嘟</string>
|
||||||
<string name="notification_boost_description">当我的嘟文被转发时通知</string>
|
<string name="notification_boost_description">当我的嘟文被转发时通知</string>
|
||||||
<string name="notification_favourite_name">收藏</string>
|
<string name="notification_favourite_name">喜欢</string>
|
||||||
<string name="notification_favourite_description">当有用户收藏了我的嘟文时通知</string>
|
<string name="notification_favourite_description">当有用户喜欢了我的嘟文时</string>
|
||||||
<string name="notification_poll_name">投票</string>
|
<string name="notification_poll_name">投票</string>
|
||||||
<string name="notification_poll_description">当我参与的投票结束时</string>
|
<string name="notification_poll_description">当我参与的投票结束时</string>
|
||||||
<string name="notification_mention_format">%s 提及了你</string>
|
<string name="notification_mention_format">%s 提及了你</string>
|
||||||
|
@ -337,13 +337,13 @@
|
||||||
<string name="unpin_action">取消置顶</string>
|
<string name="unpin_action">取消置顶</string>
|
||||||
<string name="pin_action">置顶</string>
|
<string name="pin_action">置顶</string>
|
||||||
<plurals name="favs">
|
<plurals name="favs">
|
||||||
<item quantity="other"><b>%1$s</b> 次收藏</item>
|
<item quantity="other"><b>%1$s</b> 次喜欢</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
<item quantity="other"><b>%s</b> 次转嘟</item>
|
<item quantity="other"><b>%s</b> 次转嘟</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="title_reblogged_by">转嘟</string>
|
<string name="title_reblogged_by">转嘟</string>
|
||||||
<string name="title_favourited_by">收藏</string>
|
<string name="title_favourited_by">喜欢</string>
|
||||||
<string name="conversation_1_recipients">%1$s</string>
|
<string name="conversation_1_recipients">%1$s</string>
|
||||||
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
||||||
<string name="conversation_more_recipients">%1$s,%2$s 和 %3$d 等人</string>
|
<string name="conversation_more_recipients">%1$s,%2$s 和 %3$d 等人</string>
|
||||||
|
@ -354,7 +354,7 @@
|
||||||
<string name="description_post_cw">内容警告:%s</string>
|
<string name="description_post_cw">内容警告:%s</string>
|
||||||
<string name="description_post_media_no_description_placeholder">没有描述信息</string>
|
<string name="description_post_media_no_description_placeholder">没有描述信息</string>
|
||||||
<string name="description_post_reblogged">被转嘟</string>
|
<string name="description_post_reblogged">被转嘟</string>
|
||||||
<string name="description_post_favourited">被收藏</string>
|
<string name="description_post_favourited">被喜欢</string>
|
||||||
<string name="description_visiblity_public">
|
<string name="description_visiblity_public">
|
||||||
公开
|
公开
|
||||||
</string>
|
</string>
|
||||||
|
@ -495,8 +495,8 @@
|
||||||
<string name="limit_notifications">限制时间线通知</string>
|
<string name="limit_notifications">限制时间线通知</string>
|
||||||
<string name="wellbeing_mode_notice">一些可能影响您精神状态的信息将被隐藏,这些信息包括:
|
<string name="wellbeing_mode_notice">一些可能影响您精神状态的信息将被隐藏,这些信息包括:
|
||||||
\n
|
\n
|
||||||
\n - 收藏、转发、关注通知
|
\n - 喜欢、转发、关注通知
|
||||||
\n - 收藏、转发数
|
\n - 喜欢、转发数
|
||||||
\n - 账号的已关注数量、嘟文数量
|
\n - 账号的已关注数量、嘟文数量
|
||||||
\n
|
\n
|
||||||
\n 推送通知不会被影响,但可以在通知设置中手动禁用。</string>
|
\n 推送通知不会被影响,但可以在通知设置中手动禁用。</string>
|
||||||
|
@ -516,7 +516,7 @@
|
||||||
<string name="follow_requests_info">即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。</string>
|
<string name="follow_requests_info">即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。</string>
|
||||||
<string name="dialog_delete_conversation_warning">删除此对话吗?</string>
|
<string name="dialog_delete_conversation_warning">删除此对话吗?</string>
|
||||||
<string name="action_delete_conversation">删除对话</string>
|
<string name="action_delete_conversation">删除对话</string>
|
||||||
<string name="pref_title_confirm_favourites">收藏前显示确认对话框</string>
|
<string name="pref_title_confirm_favourites">喜欢前显示确认对话框</string>
|
||||||
<string name="action_unbookmark">删除书签</string>
|
<string name="action_unbookmark">删除书签</string>
|
||||||
<string name="duration_30_days">30 天</string>
|
<string name="duration_30_days">30 天</string>
|
||||||
<string name="duration_60_days">60 天</string>
|
<string name="duration_60_days">60 天</string>
|
||||||
|
@ -536,4 +536,15 @@
|
||||||
<string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string>
|
<string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string>
|
||||||
<string name="error_could_not_load_login_page">无法加载登录页。</string>
|
<string name="error_could_not_load_login_page">无法加载登录页。</string>
|
||||||
<string name="saving_draft">正在保存草稿…</string>
|
<string name="saving_draft">正在保存草稿…</string>
|
||||||
|
<string name="title_migration_relogin">重新登陆以启用通知推送</string>
|
||||||
|
<string name="action_dismiss">不理会</string>
|
||||||
|
<string name="action_details">详情</string>
|
||||||
|
<string name="dialog_push_notification_migration_other_accounts">你已重新登录当前账户,向 Tusky 授予推送订阅权限。但是,你仍然有其他没有以这种方式迁移的账户。切换到它们,逐个重新登录,以启用 UnifiedPush 通知支持。</string>
|
||||||
|
<string name="account_date_joined">加入于%1$s</string>
|
||||||
|
<string name="tips_push_notification_migration">重新登录所有账户来启用推送通知支持。</string>
|
||||||
|
<string name="dialog_push_notification_migration">为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
<string name="action_edit_image">编辑图片</string>
|
||||||
|
<string name="error_image_edit_failed">无法编辑图片。</string>
|
||||||
|
<string name="error_loading_account_details">加载账户详情失败</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="title_posts_pinned">已置顶</string>
|
<string name="title_posts_pinned">已置顶</string>
|
||||||
<string name="title_follows">正在关注</string>
|
<string name="title_follows">正在关注</string>
|
||||||
<string name="title_followers">关注者</string>
|
<string name="title_followers">关注者</string>
|
||||||
<string name="title_favourites">收藏</string>
|
<string name="title_favourites">喜欢</string>
|
||||||
<string name="title_mutes">被隐藏的用户</string>
|
<string name="title_mutes">被隐藏的用户</string>
|
||||||
<string name="title_blocks">被屏蔽的用户</string>
|
<string name="title_blocks">被屏蔽的用户</string>
|
||||||
<string name="title_follow_requests">关注请求</string>
|
<string name="title_follow_requests">关注请求</string>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<string name="message_empty">还没有内容</string>
|
<string name="message_empty">还没有内容</string>
|
||||||
<string name="footer_empty">还没有内容,向下拉动即可刷新</string>
|
<string name="footer_empty">还没有内容,向下拉动即可刷新</string>
|
||||||
<string name="notification_reblog_format">%s 转嘟了你的嘟文</string>
|
<string name="notification_reblog_format">%s 转嘟了你的嘟文</string>
|
||||||
<string name="notification_favourite_format">%s 收藏了你的嘟文</string>
|
<string name="notification_favourite_format">%s 喜欢了你的嘟文</string>
|
||||||
<string name="notification_follow_format">%s 关注了你</string>
|
<string name="notification_follow_format">%s 关注了你</string>
|
||||||
<string name="report_username_format">报告用户 @%s 的滥用行为</string>
|
<string name="report_username_format">报告用户 @%s 的滥用行为</string>
|
||||||
<string name="report_comment_hint">报告更多信息</string>
|
<string name="report_comment_hint">报告更多信息</string>
|
||||||
|
@ -58,8 +58,8 @@
|
||||||
<string name="action_reply">回复</string>
|
<string name="action_reply">回复</string>
|
||||||
<string name="action_reblog">转嘟</string>
|
<string name="action_reblog">转嘟</string>
|
||||||
<string name="action_unreblog">取消转嘟</string>
|
<string name="action_unreblog">取消转嘟</string>
|
||||||
<string name="action_favourite">收藏</string>
|
<string name="action_favourite">喜欢</string>
|
||||||
<string name="action_unfavourite">取消收藏</string>
|
<string name="action_unfavourite">取消喜欢</string>
|
||||||
<string name="action_more">更多</string>
|
<string name="action_more">更多</string>
|
||||||
<string name="action_compose">新嘟文</string>
|
<string name="action_compose">新嘟文</string>
|
||||||
<string name="action_login">登录 Mastodon 帐号</string>
|
<string name="action_login">登录 Mastodon 帐号</string>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<string name="action_view_profile">个人资料</string>
|
<string name="action_view_profile">个人资料</string>
|
||||||
<string name="action_view_preferences">设置</string>
|
<string name="action_view_preferences">设置</string>
|
||||||
<string name="action_view_account_preferences">帐户设置</string>
|
<string name="action_view_account_preferences">帐户设置</string>
|
||||||
<string name="action_view_favourites">收藏</string>
|
<string name="action_view_favourites">喜欢</string>
|
||||||
<string name="action_view_mutes">被隐藏的用户</string>
|
<string name="action_view_mutes">被隐藏的用户</string>
|
||||||
<string name="action_view_blocks">被屏蔽的用户</string>
|
<string name="action_view_blocks">被屏蔽的用户</string>
|
||||||
<string name="action_view_follow_requests">关注请求</string>
|
<string name="action_view_follow_requests">关注请求</string>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
<string name="action_hashtags">话题</string>
|
<string name="action_hashtags">话题</string>
|
||||||
<string name="action_open_reblogger">打开转嘟用户主页</string>
|
<string name="action_open_reblogger">打开转嘟用户主页</string>
|
||||||
<string name="action_open_reblogged_by">显示转嘟</string>
|
<string name="action_open_reblogged_by">显示转嘟</string>
|
||||||
<string name="action_open_faved_by">显示收藏</string>
|
<string name="action_open_faved_by">显示喜欢</string>
|
||||||
<string name="title_hashtags_dialog">话题</string>
|
<string name="title_hashtags_dialog">话题</string>
|
||||||
<string name="title_mentions_dialog">提及</string>
|
<string name="title_mentions_dialog">提及</string>
|
||||||
<string name="title_links_dialog">链接</string>
|
<string name="title_links_dialog">链接</string>
|
||||||
|
@ -168,7 +168,7 @@
|
||||||
<string name="pref_title_notification_filter_mentions">被提及</string>
|
<string name="pref_title_notification_filter_mentions">被提及</string>
|
||||||
<string name="pref_title_notification_filter_follows">有新的关注者</string>
|
<string name="pref_title_notification_filter_follows">有新的关注者</string>
|
||||||
<string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string>
|
<string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string>
|
||||||
<string name="pref_title_notification_filter_favourites">嘟文被收藏</string>
|
<string name="pref_title_notification_filter_favourites">嘟文被喜欢</string>
|
||||||
<string name="pref_title_notification_filter_poll">投票已结束</string>
|
<string name="pref_title_notification_filter_poll">投票已结束</string>
|
||||||
<string name="pref_title_appearance_settings">外观</string>
|
<string name="pref_title_appearance_settings">外观</string>
|
||||||
<string name="pref_title_app_theme">应用主题</string>
|
<string name="pref_title_app_theme">应用主题</string>
|
||||||
|
@ -212,8 +212,8 @@
|
||||||
<string name="notification_follow_description">当有用户关注我时</string>
|
<string name="notification_follow_description">当有用户关注我时</string>
|
||||||
<string name="notification_boost_name">转嘟</string>
|
<string name="notification_boost_name">转嘟</string>
|
||||||
<string name="notification_boost_description">当有用户转嘟了我的嘟文时</string>
|
<string name="notification_boost_description">当有用户转嘟了我的嘟文时</string>
|
||||||
<string name="notification_favourite_name">收藏</string>
|
<string name="notification_favourite_name">喜欢</string>
|
||||||
<string name="notification_favourite_description">当有用户收藏了我的嘟文时</string>
|
<string name="notification_favourite_description">当有用户喜欢了我的嘟文时</string>
|
||||||
<string name="notification_poll_name">投票</string>
|
<string name="notification_poll_name">投票</string>
|
||||||
<string name="notification_poll_description">当我参与的投票结束时</string>
|
<string name="notification_poll_description">当我参与的投票结束时</string>
|
||||||
<string name="notification_mention_format">%s 提及了你</string>
|
<string name="notification_mention_format">%s 提及了你</string>
|
||||||
|
@ -333,13 +333,13 @@
|
||||||
<string name="unpin_action">取消置顶</string>
|
<string name="unpin_action">取消置顶</string>
|
||||||
<string name="pin_action">置顶</string>
|
<string name="pin_action">置顶</string>
|
||||||
<plurals name="favs">
|
<plurals name="favs">
|
||||||
<item quantity="other"><b>%1$s</b> 次收藏</item>
|
<item quantity="other"><b>%1$s</b> 次喜欢</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
<item quantity="other"><b>%s</b> 次转嘟</item>
|
<item quantity="other"><b>%s</b> 次转嘟</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="title_reblogged_by">转嘟</string>
|
<string name="title_reblogged_by">转嘟</string>
|
||||||
<string name="title_favourited_by">收藏</string>
|
<string name="title_favourited_by">喜欢</string>
|
||||||
<string name="conversation_1_recipients">%1$s</string>
|
<string name="conversation_1_recipients">%1$s</string>
|
||||||
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
||||||
<string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string>
|
<string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string>
|
||||||
|
@ -358,9 +358,7 @@
|
||||||
<string name="description_post_reblogged">
|
<string name="description_post_reblogged">
|
||||||
被转嘟
|
被转嘟
|
||||||
</string>
|
</string>
|
||||||
<string name="description_post_favourited">
|
<string name="description_post_favourited">被喜欢</string>
|
||||||
被收藏
|
|
||||||
</string>
|
|
||||||
<string name="description_visiblity_public">
|
<string name="description_visiblity_public">
|
||||||
公开
|
公开
|
||||||
</string>
|
</string>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<string name="message_empty">沒有內容。</string>
|
<string name="message_empty">沒有內容。</string>
|
||||||
<string name="footer_empty">還沒有內容,向下拉動即可重新整理!</string>
|
<string name="footer_empty">還沒有內容,向下拉動即可重新整理!</string>
|
||||||
<string name="notification_reblog_format">%s 轉嘟了你的嘟文</string>
|
<string name="notification_reblog_format">%s 轉嘟了你的嘟文</string>
|
||||||
<string name="notification_favourite_format">%s 收藏了你的嘟文</string>
|
<string name="notification_favourite_format">%s 最愛了你的嘟文</string>
|
||||||
<string name="notification_follow_format">%s 關注了你</string>
|
<string name="notification_follow_format">%s 關注了你</string>
|
||||||
<string name="report_username_format">檢舉使用者 @%s 的濫用行為</string>
|
<string name="report_username_format">檢舉使用者 @%s 的濫用行為</string>
|
||||||
<string name="report_comment_hint">更多評論?</string>
|
<string name="report_comment_hint">更多評論?</string>
|
||||||
|
@ -58,8 +58,8 @@
|
||||||
<string name="action_reply">回覆</string>
|
<string name="action_reply">回覆</string>
|
||||||
<string name="action_reblog">轉嘟</string>
|
<string name="action_reblog">轉嘟</string>
|
||||||
<string name="action_unreblog">取消轉嘟</string>
|
<string name="action_unreblog">取消轉嘟</string>
|
||||||
<string name="action_favourite">收藏</string>
|
<string name="action_favourite">最愛</string>
|
||||||
<string name="action_unfavourite">取消收藏</string>
|
<string name="action_unfavourite">取消最愛</string>
|
||||||
<string name="action_more">更多</string>
|
<string name="action_more">更多</string>
|
||||||
<string name="action_compose">撰寫嘟文</string>
|
<string name="action_compose">撰寫嘟文</string>
|
||||||
<string name="action_login">登入 Mastodon 帳號</string>
|
<string name="action_login">登入 Mastodon 帳號</string>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<string name="action_view_profile">個人資料</string>
|
<string name="action_view_profile">個人資料</string>
|
||||||
<string name="action_view_preferences">設定</string>
|
<string name="action_view_preferences">設定</string>
|
||||||
<string name="action_view_account_preferences">帳戶設定</string>
|
<string name="action_view_account_preferences">帳戶設定</string>
|
||||||
<string name="action_view_favourites">我的收藏</string>
|
<string name="action_view_favourites">我的最愛</string>
|
||||||
<string name="action_view_mutes">被靜音的使用者</string>
|
<string name="action_view_mutes">被靜音的使用者</string>
|
||||||
<string name="action_view_blocks">被封鎖的使用者</string>
|
<string name="action_view_blocks">被封鎖的使用者</string>
|
||||||
<string name="action_view_follow_requests">關注請求</string>
|
<string name="action_view_follow_requests">關注請求</string>
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
<string name="pref_title_notification_filter_mentions">被提及</string>
|
<string name="pref_title_notification_filter_mentions">被提及</string>
|
||||||
<string name="pref_title_notification_filter_follows">有新的關注者</string>
|
<string name="pref_title_notification_filter_follows">有新的關注者</string>
|
||||||
<string name="pref_title_notification_filter_reblogs">嘟文被轉嘟</string>
|
<string name="pref_title_notification_filter_reblogs">嘟文被轉嘟</string>
|
||||||
<string name="pref_title_notification_filter_favourites">嘟文被加入收藏</string>
|
<string name="pref_title_notification_filter_favourites">嘟文被加入最愛</string>
|
||||||
<string name="pref_title_notification_filter_poll">投票已結束</string>
|
<string name="pref_title_notification_filter_poll">投票已結束</string>
|
||||||
<string name="pref_title_appearance_settings">外觀</string>
|
<string name="pref_title_appearance_settings">外觀</string>
|
||||||
<string name="pref_title_app_theme">佈景主題</string>
|
<string name="pref_title_app_theme">佈景主題</string>
|
||||||
|
@ -215,8 +215,8 @@
|
||||||
<string name="notification_follow_description">當有使用者關注我時</string>
|
<string name="notification_follow_description">當有使用者關注我時</string>
|
||||||
<string name="notification_boost_name">轉嘟</string>
|
<string name="notification_boost_name">轉嘟</string>
|
||||||
<string name="notification_boost_description">當有使用者轉嘟了我的嘟文時</string>
|
<string name="notification_boost_description">當有使用者轉嘟了我的嘟文時</string>
|
||||||
<string name="notification_favourite_name">收藏</string>
|
<string name="notification_favourite_name">最愛</string>
|
||||||
<string name="notification_favourite_description">當有使用者把我的嘟文加入收藏時</string>
|
<string name="notification_favourite_description">當有使用者把我的嘟文加入最愛時</string>
|
||||||
<string name="notification_poll_name">投票</string>
|
<string name="notification_poll_name">投票</string>
|
||||||
<string name="notification_poll_description">當我參與的投票結束時</string>
|
<string name="notification_poll_description">當我參與的投票結束時</string>
|
||||||
<string name="notification_mention_format">%s 提及了你</string>
|
<string name="notification_mention_format">%s 提及了你</string>
|
||||||
|
@ -335,13 +335,13 @@
|
||||||
<string name="unpin_action">取消置頂</string>
|
<string name="unpin_action">取消置頂</string>
|
||||||
<string name="pin_action">置頂</string>
|
<string name="pin_action">置頂</string>
|
||||||
<plurals name="favs">
|
<plurals name="favs">
|
||||||
<item quantity="other"><b>%1$s</b> 次收藏</item>
|
<item quantity="other"><b>%1$s</b> 次最愛</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
<item quantity="other"><b>%s</b> 次轉嘟</item>
|
<item quantity="other"><b>%s</b> 次轉嘟</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="title_reblogged_by">轉嘟</string>
|
<string name="title_reblogged_by">轉嘟</string>
|
||||||
<string name="title_favourited_by">收藏由</string>
|
<string name="title_favourited_by">最愛由</string>
|
||||||
<string name="conversation_1_recipients">%1$s</string>
|
<string name="conversation_1_recipients">%1$s</string>
|
||||||
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
<string name="conversation_2_recipients">%1$s 和 %2$s</string>
|
||||||
<string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string>
|
<string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string>
|
||||||
|
@ -360,9 +360,7 @@
|
||||||
<string name="description_post_reblogged">
|
<string name="description_post_reblogged">
|
||||||
被轉嘟
|
被轉嘟
|
||||||
</string>
|
</string>
|
||||||
<string name="description_post_favourited">
|
<string name="description_post_favourited">被最愛</string>
|
||||||
被收藏
|
|
||||||
</string>
|
|
||||||
<string name="description_visiblity_public">
|
<string name="description_visiblity_public">
|
||||||
公開
|
公開
|
||||||
</string>
|
</string>
|
||||||
|
@ -444,8 +442,8 @@
|
||||||
<string name="review_notifications">檢查通知設定</string>
|
<string name="review_notifications">檢查通知設定</string>
|
||||||
<string name="wellbeing_mode_notice">有些資訊可能會影響你的心理健康將會被隱藏。包括:
|
<string name="wellbeing_mode_notice">有些資訊可能會影響你的心理健康將會被隱藏。包括:
|
||||||
\n
|
\n
|
||||||
\n- 收藏/轉嘟/關注 通知
|
\n- 最愛/轉嘟/關注 通知
|
||||||
\n- 收藏/轉嘟 數量
|
\n- 最愛/轉嘟 數量
|
||||||
\n- 關注/貼文 在個人頁面的狀態
|
\n- 關注/貼文 在個人頁面的狀態
|
||||||
\n
|
\n
|
||||||
\n推播通知不會受到影響,但你可以手動檢查你的通知設定。</string>
|
\n推播通知不會受到影響,但你可以手動檢查你的通知設定。</string>
|
||||||
|
|
|
@ -9,11 +9,13 @@
|
||||||
<string name="error_authorization_unknown">An unidentified authorization error occurred.</string>
|
<string name="error_authorization_unknown">An unidentified authorization error occurred.</string>
|
||||||
<string name="error_authorization_denied">Authorization was denied.</string>
|
<string name="error_authorization_denied">Authorization was denied.</string>
|
||||||
<string name="error_retrieving_oauth_token">Failed getting a login token.</string>
|
<string name="error_retrieving_oauth_token">Failed getting a login token.</string>
|
||||||
|
<string name="error_loading_account_details">Failed loading account details</string>
|
||||||
<string name="error_could_not_load_login_page">Could not load the login page.</string>
|
<string name="error_could_not_load_login_page">Could not load the login page.</string>
|
||||||
<string name="error_compose_character_limit">The post is too long!</string>
|
<string name="error_compose_character_limit">The post is too long!</string>
|
||||||
<string name="error_image_upload_size">The file must be less than 8MB.</string>
|
<string name="error_image_upload_size">The file must be less than 8MB.</string>
|
||||||
<string name="error_video_upload_size">Video files must be less than 40MB.</string>
|
<string name="error_video_upload_size">Video files must be less than 40MB.</string>
|
||||||
<string name="error_audio_upload_size">Audio files must be less than 40MB.</string>
|
<string name="error_audio_upload_size">Audio files must be less than 40MB.</string>
|
||||||
|
<string name="error_image_edit_failed">The image could not be edited.</string>
|
||||||
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
|
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
|
||||||
<string name="error_media_upload_opening">That file could not be opened.</string>
|
<string name="error_media_upload_opening">That file could not be opened.</string>
|
||||||
<string name="error_media_upload_permission">Permission to read media is required.</string>
|
<string name="error_media_upload_permission">Permission to read media is required.</string>
|
||||||
|
@ -378,6 +380,7 @@
|
||||||
<string name="post_media_video">Video</string>
|
<string name="post_media_video">Video</string>
|
||||||
<string name="post_media_audio">Audio</string>
|
<string name="post_media_audio">Audio</string>
|
||||||
<string name="post_media_attachments">Attachments</string>
|
<string name="post_media_attachments">Attachments</string>
|
||||||
|
<string name="status_count_one_plus">1+</string>
|
||||||
|
|
||||||
<string name="state_follow_requested">Follow requested</string>
|
<string name="state_follow_requested">Follow requested</string>
|
||||||
|
|
||||||
|
@ -434,6 +437,7 @@
|
||||||
<item quantity="other">Describe for visually impaired\n(%d character limit)</item>
|
<item quantity="other">Describe for visually impaired\n(%d character limit)</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="action_set_caption">Set caption</string>
|
<string name="action_set_caption">Set caption</string>
|
||||||
|
<string name="action_edit_image">Edit image</string>
|
||||||
<string name="action_remove">Remove</string>
|
<string name="action_remove">Remove</string>
|
||||||
<string name="lock_account_label">Lock account</string>
|
<string name="lock_account_label">Lock account</string>
|
||||||
<string name="lock_account_label_description">Requires you to manually approve followers</string>
|
<string name="lock_account_label_description">Requires you to manually approve followers</string>
|
||||||
|
|
|
@ -74,6 +74,7 @@ class BottomSheetActivityTest {
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
favouritesCount = 0,
|
favouritesCount = 0,
|
||||||
|
repliesCount = 0,
|
||||||
reblogged = false,
|
reblogged = false,
|
||||||
favourited = false,
|
favourited = false,
|
||||||
bookmarked = false,
|
bookmarked = false,
|
||||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Intent
|
||||||
import android.os.Looper.getMainLooper
|
import android.os.Looper.getMainLooper
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
|
@ -67,6 +68,8 @@ class ComposeActivityTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = instanceDomain,
|
domain = instanceDomain,
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true,
|
isActive = true,
|
||||||
accountId = "1",
|
accountId = "1",
|
||||||
username = "username",
|
username = "username",
|
||||||
|
@ -95,12 +98,12 @@ class ComposeActivityTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
apiMock = mock {
|
apiMock = mock {
|
||||||
onBlocking { getCustomEmojis() } doReturn Result.success(emptyList())
|
onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList())
|
||||||
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
Result.failure(Throwable())
|
NetworkResult.failure(Throwable())
|
||||||
} else {
|
} else {
|
||||||
Result.success(instance)
|
NetworkResult.success(instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,7 @@ class FilterTest {
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
favouritesCount = 0,
|
favouritesCount = 0,
|
||||||
|
repliesCount = 0,
|
||||||
reblogged = false,
|
reblogged = false,
|
||||||
favourited = false,
|
favourited = false,
|
||||||
bookmarked = false,
|
bookmarked = false,
|
||||||
|
|
|
@ -46,6 +46,8 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = "mastodon.example",
|
domain = "mastodon.example",
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true
|
isActive = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ class NetworkTimelineRemoteMediatorTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = "mastodon.example",
|
domain = "mastodon.example",
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true
|
isActive = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ fun mockStatus(id: String = "100") = Status(
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
reblogsCount = 1,
|
reblogsCount = 1,
|
||||||
favouritesCount = 2,
|
favouritesCount = 2,
|
||||||
|
repliesCount = 3,
|
||||||
reblogged = false,
|
reblogged = false,
|
||||||
favourited = true,
|
favourited = true,
|
||||||
bookmarked = true,
|
bookmarked = true,
|
||||||
|
|
|
@ -443,6 +443,7 @@ class TimelineDaoTest {
|
||||||
emojis = "emojis$statusId",
|
emojis = "emojis$statusId",
|
||||||
reblogsCount = 1 * statusId.toInt(),
|
reblogsCount = 1 * statusId.toInt(),
|
||||||
favouritesCount = 2 * statusId.toInt(),
|
favouritesCount = 2 * statusId.toInt(),
|
||||||
|
repliesCount = 3 * statusId.toInt(),
|
||||||
reblogged = even,
|
reblogged = even,
|
||||||
favourited = !even,
|
favourited = !even,
|
||||||
bookmarked = false,
|
bookmarked = false,
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.mockwebserver.MockResponse
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.kotlin.doAnswer
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
|
||||||
|
class InstanceSwitchAuthInterceptorTest {
|
||||||
|
|
||||||
|
private val mockWebServer = MockWebServer()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockWebServer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
mockWebServer.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should make regular request when requested`() {
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse())
|
||||||
|
|
||||||
|
val accountManager: AccountManager = mock {
|
||||||
|
on { activeAccount } doAnswer { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url(mockWebServer.url("/test"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
assertEquals(200, response.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should make request to instance requested in special header`() {
|
||||||
|
mockWebServer.enqueue(MockResponse())
|
||||||
|
|
||||||
|
val accountManager: AccountManager = mock {
|
||||||
|
on { activeAccount } doAnswer {
|
||||||
|
AccountEntity(
|
||||||
|
id = 1,
|
||||||
|
domain = "test.domain",
|
||||||
|
accessToken = "fakeToken",
|
||||||
|
clientId = "fakeId",
|
||||||
|
clientSecret = "fakeSecret",
|
||||||
|
isActive = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
|
||||||
|
.header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
assertEquals(200, response.code)
|
||||||
|
|
||||||
|
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should make request to current instance when requested and user is logged in`() {
|
||||||
|
mockWebServer.enqueue(MockResponse())
|
||||||
|
|
||||||
|
val accountManager: AccountManager = mock {
|
||||||
|
on { activeAccount } doAnswer {
|
||||||
|
AccountEntity(
|
||||||
|
id = 1,
|
||||||
|
domain = mockWebServer.hostName,
|
||||||
|
accessToken = "fakeToken",
|
||||||
|
clientId = "fakeId",
|
||||||
|
clientSecret = "fakeSecret",
|
||||||
|
isActive = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
assertEquals(200, response.code)
|
||||||
|
|
||||||
|
assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail to make request when request to current instance is requested but no user is logged in`() {
|
||||||
|
mockWebServer.enqueue(MockResponse())
|
||||||
|
|
||||||
|
val accountManager: AccountManager = mock {
|
||||||
|
on { activeAccount } doAnswer { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
assertEquals(400, response.code)
|
||||||
|
assertEquals(0, mockWebServer.requestCount)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue