diff --git a/README.md b/README.md index 80aac459..021901b2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It is derived from [Tusky](https://tusky.app) and has been modified to reflect C - Material Design - Multi-Account support - Respect device preferences for light/dark theming -- Drafts - compose toots and save them for later +- Drafts - compose posts and save them for later - Choose between different emoji styles - Optimized for all screen sizes - Completely open-source - no non-free dependencies like Google services @@ -22,7 +22,7 @@ It is derived from [Tusky](https://tusky.app) and has been modified to reflect C ### Support Check out Tusky's [FAQs](https://github.com/tuskyapp/faq), your question may already be answered. -If you have any bug reports, feature requests or questions please open an issue or send us a toot at [chinwagnews@chinwag.org](https://social.chinwag.org/@ChinwagNews)! +If you have any bug reports, feature requests or questions please open an issue or send us a message at [chinwagnews@chinwag.org](https://social.chinwag.org/@ChinwagNews)! For translating Tusky into your language, visit https://weblate.tusky.app/ diff --git a/app/build.gradle b/app/build.gradle index 36d9e011..b153ae0b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,13 +15,13 @@ def getGitSha = { } android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId APP_ID minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 87 - versionName "16.0-CW1" + versionName "17.0-CW1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -90,12 +90,12 @@ android { } ext.coroutinesVersion = "1.6.0" -ext.lifecycleVersion = "2.3.1" -ext.roomVersion = '2.3.0' +ext.lifecycleVersion = "2.4.1" +ext.roomVersion = '2.4.2' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.3' -ext.glideVersion = '4.12.0' -ext.daggerVersion = '2.40.5' +ext.glideVersion = '4.13.1' +ext.daggerVersion = '2.41' ext.materialdrawerVersion = '8.4.5' // if libraries are changed here, they should also be changed in LicenseActivity @@ -105,33 +105,35 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion" - implementation "androidx.core:core-ktx:1.5.0" - implementation "androidx.appcompat:appcompat:1.3.0" - implementation "androidx.fragment:fragment-ktx:1.3.4" - implementation "androidx.browser:browser:1.3.0" + implementation "androidx.core:core-ktx:1.7.0" + implementation "androidx.appcompat:appcompat:1.4.1" + implementation "androidx.fragment:fragment-ktx:1.4.1" + implementation "androidx.browser:browser:1.4.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.exifinterface:exifinterface:1.3.3" implementation "androidx.cardview:cardview:1.0.0" - implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.sharetarget:sharetarget:1.1.0" + implementation "androidx.preference:preference-ktx:1.2.0" + implementation "androidx.sharetarget:sharetarget:1.2.0-rc01" implementation "androidx.emoji:emoji:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" - implementation "androidx.constraintlayout:constraintlayout:2.1.2" - implementation "androidx.paging:paging-runtime-ktx:3.0.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation "androidx.paging:paging-runtime-ktx:3.1.1" implementation "androidx.viewpager2:viewpager2:1.0.0" - implementation "androidx.work:work-runtime:2.5.0" + implementation "androidx.work:work-runtime:2.7.1" implementation "androidx.room:room-ktx:$roomVersion" + implementation "androidx.room:room-paging:$roomVersion" implementation "androidx.room:room-rxjava3:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" + implementation 'androidx.core:core-splashscreen:1.0.0-beta02' - implementation "com.google.android.material:material:1.4.0" + implementation "com.google.android.material:material:1.5.0" - implementation "com.google.code.gson:gson:2.8.9" + implementation "com.google.code.gson:gson:2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" @@ -146,14 +148,14 @@ dependencies { implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion" - implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0" + implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0" - implementation "io.reactivex.rxjava3:rxjava:3.0.12" + implementation "io.reactivex.rxjava3:rxjava:3.1.3" implementation "io.reactivex.rxjava3:rxandroid:3.0.0" implementation "io.reactivex.rxjava3:rxkotlin:3.0.1" - implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0" - implementation "com.uber.autodispose2:autodispose:2.0.0" + implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1" + implementation "com.uber.autodispose2:autodispose:2.1.1" implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" @@ -169,7 +171,7 @@ dependencies { implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' - implementation "com.github.CanHub:Android-Image-Cropper:3.1.0" + implementation "com.github.CanHub:Android-Image-Cropper:4.1.0" implementation "de.c1710:filemojicompat:1.0.18" diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json new file mode 100644 index 00000000..654211ae --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json @@ -0,0 +1,789 @@ +{ + "formatVersion": 1, + "database": { + "version": 29, + "identityHash": "62c289344334da2db091ad4ba0a49c6a", + "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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "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, `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": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '62c289344334da2db091ad4ba0a49c6a')" + ] + } +} diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json new file mode 100644 index 00000000..e0a9e9f6 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json @@ -0,0 +1,807 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "a75615171612bdfc9e3d4201ebf6071a", + "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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json new file mode 100644 index 00000000..c705293c --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 31, + "identityHash": "a75615171612bdfc9e3d4201ebf6071a", + "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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')" + ] + } +} \ No newline at end of file diff --git a/app/src/green/res/values/flavor-colors.xml b/app/src/green/res/values/flavor-colors.xml new file mode 100644 index 00000000..e1f58f2e --- /dev/null +++ b/app/src/green/res/values/flavor-colors.xml @@ -0,0 +1,6 @@ + + + + #19A341 + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30751a31..391a8765 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,9 +20,12 @@ android:supportsRtl="true" android:theme="@style/TuskyTheme" android:usesCleartextTraffic="false"> + + android:theme="@style/SplashTheme" + android:exported="true"> + @@ -35,22 +38,15 @@ - - - - - - - - + + android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" + android:exported="true"> + @@ -106,7 +102,6 @@ - @@ -124,7 +119,8 @@ android:theme="@style/Base.Theme.AppCompat" /> + android:launchMode="singleTop" + android:exported="false"> @@ -134,18 +130,18 @@ android:resource="@xml/searchable" /> - - + - + - + + tools:node="merge"> + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png deleted file mode 100644 index bb5a0ed1..00000000 Binary files a/app/src/main/ic_launcher-web.png and /dev/null differ diff --git a/app/src/main/ic_launcher.png b/app/src/main/ic_launcher.png new file mode 100644 index 00000000..441334e2 Binary files /dev/null and b/app/src/main/ic_launcher.png differ diff --git a/app/src/main/ic_launcher.svg b/app/src/main/ic_launcher.svg index fc0fc592..37093a15 100644 --- a/app/src/main/ic_launcher.svg +++ b/app/src/main/ic_launcher.svg @@ -1,488 +1,101 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 02fa0738..03a74458 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -34,7 +34,7 @@ import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.Either @@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import java.io.IOException import javax.inject.Inject -private typealias AccountInfo = Pair +private typealias AccountInfo = Pair class AccountsInListFragment : DialogFragment(), Injectable { @@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable { viewModel.deleteAccountFromList(listId, accountId) } - private fun onAddToList(account: Account) { + private fun onAddToList(account: TimelineAccount) { viewModel.addAccountToList(listId, account) } - private object AccountDiffer : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean { - return oldItem == newItem + private object AccountDiffer : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean { - return oldItem.deepEquals(newItem) + override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean { + return oldItem == newItem } } - inner class Adapter : ListAdapter>(AccountDiffer) { + inner class Adapter : ListAdapter>(AccountDiffer) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable { private object SearchDiffer : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { - return oldItem == newItem + return oldItem.first.id == newItem.first.id } override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { - return oldItem.second == newItem.second && - oldItem.first.deepEquals(newItem.first) + return oldItem == newItem } } diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index e348f036..d34dd6df 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -38,6 +38,7 @@ import androidx.preference.PreferenceManager; import com.google.android.material.snackbar.Snackbar; import com.keylesspalace.tusky.adapter.AccountSelectionAdapter; +import com.keylesspalace.tusky.components.login.LoginActivity; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; @@ -197,6 +198,33 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab .show(); } + public @Nullable String getOpenAsText() { + List accounts = accountManager.getAllAccountsOrderedByActive(); + switch (accounts.size()) { + case 0: + case 1: + return null; + case 2: + for (AccountEntity account : accounts) { + if (account != accountManager.getActiveAccount()) { + return String.format(getString(R.string.action_open_as), account.getFullName()); + } + } + return null; + default: + return String.format(getString(R.string.action_open_as), "…"); + } + } + + public void openAsAccount(@NonNull String url, @NonNull AccountEntity account) { + accountManager.setActiveAccount(account); + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra(MainActivity.REDIRECT_URL, url); + startActivity(intent); + finishWithoutSlideOutAnimation(); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt index 599322a0..046ab04a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -15,6 +15,7 @@ package com.keylesspalace.tusky +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View @@ -27,7 +28,7 @@ import autodispose2.autoDispose import com.google.android.material.bottomsheet.BottomSheetBehavior import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.openLink import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import java.net.URI import java.net.URISyntaxException @@ -157,9 +158,9 @@ abstract class BottomSheetActivity : BaseActivity() { } } - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) open fun openLink(url: String) { - LinkHelper.openLink(url, this) + (this as Context).openLink(url) } private fun showQuerySheet() { diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index ef0e3e98..b749fe93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -15,28 +15,26 @@ package com.keylesspalace.tusky -import android.Manifest -import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Color import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView import androidx.activity.viewModels -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.LiveData import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.options import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding @@ -44,9 +42,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading -import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success -import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewmodel.EditProfileViewModel @@ -63,12 +59,7 @@ class EditProfileActivity : BaseActivity(), Injectable { const val HEADER_WIDTH = 1500 const val HEADER_HEIGHT = 500 - private const val AVATAR_PICK_RESULT = 1 - private const val HEADER_PICK_RESULT = 2 - private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 private const val MAX_ACCOUNT_FIELDS = 4 - - private const val BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING" } @Inject @@ -78,23 +69,28 @@ class EditProfileActivity : BaseActivity(), Injectable { private val binding by viewBinding(ActivityEditProfileBinding::inflate) - private var currentlyPicking: PickType = PickType.NOTHING - private val accountFieldEditAdapter = AccountFieldEditAdapter() private enum class PickType { - NOTHING, AVATAR, HEADER } + private val cropImage = registerForActivityResult(CropImageContract()) { result -> + if (result.isSuccessful) { + if (result.uriContent == viewModel.getAvatarUri()) { + viewModel.newAvatarPicked() + } else { + viewModel.newHeaderPicked() + } + } else { + onPickFailure(result.error) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let { - currentlyPicking = PickType.valueOf(it) - } - setContentView(binding.root) setSupportActionBar(binding.includedToolbar.toolbar) @@ -104,8 +100,8 @@ class EditProfileActivity : BaseActivity(), Injectable { setDisplayShowHomeEnabled(true) } - binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } - binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } + binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) } + binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) } binding.fieldList.layoutManager = LinearLayoutManager(this) binding.fieldList.adapter = accountFieldEditAdapter @@ -159,11 +155,11 @@ class EditProfileActivity : BaseActivity(), Injectable { } } is Error -> { - val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) - snackbar.setAction(R.string.action_retry) { - viewModel.obtainProfile() - } - snackbar.show() + Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { + viewModel.obtainProfile() + } + .show() } is Loading -> { } } @@ -179,30 +175,24 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true) - observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false) + observeImage(viewModel.avatarData, binding.avatarPreview, true) + observeImage(viewModel.headerData, binding.headerPreview, false) viewModel.saveData.observe( - this, - { - when (it) { - is Success -> { - finish() - } - is Loading -> { - binding.saveProgressBar.visibility = View.VISIBLE - } - is Error -> { - onSaveFailure(it.errorMessage) - } + this + ) { + when (it) { + is Success -> { + finish() + } + is Loading -> { + binding.saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) } } - ) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString()) + } } override fun onStop() { @@ -218,90 +208,60 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun observeImage( - liveData: LiveData>, + liveData: LiveData, imageView: ImageView, - progressBar: View, roundedCorners: Boolean ) { liveData.observe( - this, - { + this + ) { imageUri -> - when (it) { - is Success -> { - val glide = Glide.with(imageView) - .load(it.data) + // skipping all caches so we can always reuse the same uri + val glide = Glide.with(imageView) + .load(imageUri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) - if (roundedCorners) { - glide.transform( - FitCenter(), - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) - ) - } - - glide.into(imageView) - - imageView.show() - progressBar.hide() - } - is Loading -> { - progressBar.show() - } - is Error -> { - progressBar.hide() - if (!it.consumed) { - onResizeFailure() - it.consumed = true - } - } - } + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ).into(imageView) + } else { + glide.into(imageView) } - ) - } - private fun onMediaPick(pickType: PickType) { - if (currentlyPicking != PickType.NOTHING) { - // Ignore inputs if another pick operation is still occurring. - return - } - currentlyPicking = pickType - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) - } else { - initiateMediaPicking() + imageView.show() } } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - when (requestCode) { - PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initiateMediaPicking() - } else { - endMediaPicking() - Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() - } - } - } - } - - private fun initiateMediaPicking() { + private fun pickMedia(pickType: PickType) { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "image/*" - when (currentlyPicking) { + when (pickType) { PickType.AVATAR -> { - startActivityForResult(intent, AVATAR_PICK_RESULT) + cropImage.launch( + options { + setRequestedSize(AVATAR_SIZE, AVATAR_SIZE) + setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) + setImageSource(includeGallery = true, includeCamera = false) + setOutputUri(viewModel.getAvatarUri()) + setOutputCompressFormat(Bitmap.CompressFormat.PNG) + } + ) } PickType.HEADER -> { - startActivityForResult(intent, HEADER_PICK_RESULT) + cropImage.launch( + options { + setRequestedSize(HEADER_WIDTH, HEADER_HEIGHT) + setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) + setImageSource(includeGallery = true, includeCamera = false) + setOutputUri(viewModel.getHeaderUri()) + setOutputCompressFormat(Bitmap.CompressFormat.PNG) + } + ) } - PickType.NOTHING -> { /* do nothing */ } } } @@ -321,16 +281,11 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun save() { - if (currentlyPicking != PickType.NOTHING) { - return - } - viewModel.save( binding.displayNameEditText.text.toString(), binding.noteEditText.text.toString(), binding.lockedCheckBox.isChecked, - accountFieldEditAdapter.getFieldData(), - this + accountFieldEditAdapter.getFieldData() ) } @@ -340,90 +295,8 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.saveProgressBar.visibility = View.GONE } - private fun beginMediaPicking() { - when (currentlyPicking) { - PickType.AVATAR -> { - binding.avatarProgressBar.visibility = View.VISIBLE - binding.avatarPreview.visibility = View.INVISIBLE - binding.avatarButton.setImageDrawable(null) - } - PickType.HEADER -> { - binding.headerProgressBar.visibility = View.VISIBLE - binding.headerPreview.visibility = View.INVISIBLE - binding.headerButton.setImageDrawable(null) - } - PickType.NOTHING -> { /* do nothing */ } - } - } - - private fun endMediaPicking() { - binding.avatarProgressBar.visibility = View.GONE - binding.headerProgressBar.visibility = View.GONE - - currentlyPicking = PickType.NOTHING - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - AVATAR_PICK_RESULT -> { - if (resultCode == Activity.RESULT_OK && data != null) { - CropImage.activity(data.data) - .setInitialCropWindowPaddingRatio(0f) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) - .start(this) - } else { - endMediaPicking() - } - } - HEADER_PICK_RESULT -> { - if (resultCode == Activity.RESULT_OK && data != null) { - CropImage.activity(data.data) - .setInitialCropWindowPaddingRatio(0f) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) - .start(this) - } else { - endMediaPicking() - } - } - CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> { - val result = CropImage.getActivityResult(data) - when (resultCode) { - Activity.RESULT_OK -> beginResize(result?.uriContent) - CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure() - else -> endMediaPicking() - } - } - } - } - - private fun beginResize(uri: Uri?) { - if (uri == null) { - currentlyPicking = PickType.NOTHING - return - } - - beginMediaPicking() - - when (currentlyPicking) { - PickType.AVATAR -> { - viewModel.newAvatar(uri, this) - } - PickType.HEADER -> { - viewModel.newHeader(uri, this) - } - else -> { - throw AssertionError("PickType not set.") - } - } - - currentlyPicking = PickType.NOTHING - } - - private fun onResizeFailure() { + private fun onPickFailure(throwable: Throwable?) { + Log.w("EditProfileActivity", "failed to pick media", throwable) Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() - endMediaPicking() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 3bd0d498..f2f7b38e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -40,7 +40,6 @@ import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar -import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -201,7 +200,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun onListSelected(listId: String) { startActivityWithSlideInAnimation( - ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId) + StatusListActivity.newListIntent(this, listId) ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt deleted file mode 100644 index 859bac61..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ /dev/null @@ -1,381 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.util.Log -import android.view.View -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent -import com.bumptech.glide.Glide -import com.keylesspalace.tusky.databinding.ActivityLoginBinding -import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.entity.AccessToken -import com.keylesspalace.tusky.entity.AppCredentials -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.ThemeUtils -import com.keylesspalace.tusky.util.getNonNullString -import com.keylesspalace.tusky.util.rickRoll -import com.keylesspalace.tusky.util.shouldRickRoll -import com.keylesspalace.tusky.util.viewBinding -import okhttp3.HttpUrl -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import javax.inject.Inject - -class LoginActivity : BaseActivity(), Injectable { - - @Inject - lateinit var mastodonApi: MastodonApi - - private val binding by viewBinding(ActivityLoginBinding::inflate) - - private lateinit var preferences: SharedPreferences - - private val oauthRedirectUri: String - get() { - val scheme = getString(R.string.oauth_scheme) - val host = BuildConfig.APPLICATION_ID - return "$scheme://$host/" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(binding.root) - - if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { - binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) - binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) - } - - if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { - Glide.with(binding.loginLogo) - .load(BuildConfig.CUSTOM_LOGO_URL) - .placeholder(null) - .into(binding.loginLogo) - } - - preferences = getSharedPreferences( - getString(R.string.preferences_file_key), Context.MODE_PRIVATE - ) - - binding.loginButton.setOnClickListener { onButtonClick() } - binding.registerButton.setOnClickListener { onRegisterClick() } - - binding.whatsAnInstanceTextView.setOnClickListener { - val dialog = AlertDialog.Builder(this) - .setMessage(R.string.dialog_whats_an_instance) - .setPositiveButton(R.string.action_close, null) - .show() - val textView = dialog.findViewById(android.R.id.message) - textView?.movementMethod = LinkMovementMethod.getInstance() - } - - if (isAdditionalLogin()) { - setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowTitleEnabled(false) - } else { - binding.toolbar.visibility = View.GONE - } - } - - override fun requiresLogin(): Boolean { - return false - } - - override fun finish() { - super.finish() - if (isAdditionalLogin()) { - overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) - } - } - - private fun onRegisterClick() { - binding.registerButton.isEnabled = false - - val uri = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL) - if (!openInCustomTab(uri, this)) { - val viewIntent = Intent(Intent.ACTION_VIEW, uri) - if (viewIntent.resolveActivity(packageManager) != null) { - startActivity(viewIntent) - } else { - binding.domainEditText.error = getString(R.string.error_no_web_browser_found) - setLoading(false) - } - } - } - - /** - * Obtain the oauth client credentials for this app. This is only necessary the first time the - * app is run on a given server instance. So, after the first authentication, they are - * saved in SharedPreferences and every subsequent run they are simply fetched from there. - */ - private fun onButtonClick() { - - binding.loginButton.isEnabled = false - - val domain = canonicalizeDomain(binding.domainEditText.text.toString()) - - try { - HttpUrl.Builder().host(domain).scheme("https").build() - } catch (e: IllegalArgumentException) { - setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain) - return - } - - if (shouldRickRoll(this, domain)) { - rickRoll(this) - return - } - - val callback = object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - if (!response.isSuccessful) { - binding.loginButton.isEnabled = true - binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) - setLoading(false) - Log.e(TAG, "App authentication failed. " + response.message()) - return - } - val credentials = response.body() - val clientId = credentials!!.clientId - val clientSecret = credentials.clientSecret - - preferences.edit() - .putString("domain", domain) - .putString("clientId", clientId) - .putString("clientSecret", clientSecret) - .apply() - - redirectUserToAuthorizeAndLogin(domain, clientId) - } - - override fun onFailure(call: Call, t: Throwable) { - binding.loginButton.isEnabled = true - binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) - setLoading(false) - Log.e(TAG, Log.getStackTraceString(t)) - } - } - - mastodonApi - .authenticateApp( - domain, getString(R.string.app_name), oauthRedirectUri, - OAUTH_SCOPES, getString(R.string.tusky_website) - ) - .enqueue(callback) - setLoading(true) - } - - private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { - /* To authorize this app and log in it's necessary to redirect to the domain given, - * login there, and the server will redirect back to the app with its response. */ - val endpoint = MastodonApi.ENDPOINT_AUTHORIZE - val parameters = mapOf( - "client_id" to clientId, - "redirect_uri" to oauthRedirectUri, - "response_type" to "code", - "scope" to OAUTH_SCOPES - ) - val url = "https://" + domain + endpoint + "?" + toQueryString(parameters) - val uri = Uri.parse(url) - if (!openInCustomTab(uri, this)) { - val viewIntent = Intent(Intent.ACTION_VIEW, uri) - if (viewIntent.resolveActivity(packageManager) != null) { - startActivity(viewIntent) - } else { - binding.domainEditText.error = getString(R.string.error_no_web_browser_found) - setLoading(false) - } - } - } - - override fun onStart() { - super.onStart() - /* Check if we are resuming during authorization by seeing if the intent contains the - * redirect that was given to the server. If so, its response is here! */ - val uri = intent.data - val redirectUri = oauthRedirectUri - - if (uri != null && uri.toString().startsWith(redirectUri)) { - // This should either have returned an authorization code or an error. - val code = uri.getQueryParameter("code") - val error = uri.getQueryParameter("error") - - /* restore variables from SharedPreferences */ - val domain = preferences.getNonNullString(DOMAIN, "") - val clientId = preferences.getNonNullString(CLIENT_ID, "") - val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "") - - if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) { - - setLoading(true) - /* Since authorization has succeeded, the final step to log in is to exchange - * the authorization code for an access token. */ - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onLoginSuccess(response.body()!!.accessToken, domain) - } else { - setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) - Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message())) - } - } - - override fun onFailure(call: Call, t: Throwable) { - setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) - Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message)) - } - } - - mastodonApi.fetchOAuthToken( - domain, clientId, clientSecret, redirectUri, code, - "authorization_code" - ).enqueue(callback) - } else if (error != null) { - /* Authorization failed. Put the error response where the user can read it and they - * can try again. */ - setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) - Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error)) - } else { - // This case means a junk response was received somehow. - setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_authorization_unknown) - } - } else { - // first show or user cancelled login - setLoading(false) - } - } - - private fun setLoading(loadingState: Boolean) { - if (loadingState) { - binding.loginLoadingLayout.visibility = View.VISIBLE - binding.loginInputLayout.visibility = View.GONE - } else { - binding.loginLoadingLayout.visibility = View.GONE - binding.loginInputLayout.visibility = View.VISIBLE - binding.loginButton.isEnabled = true - } - } - - private fun isAdditionalLogin(): Boolean { - return intent.getBooleanExtra(LOGIN_MODE, false) - } - - private fun onLoginSuccess(accessToken: String, domain: String) { - - setLoading(true) - - accountManager.addAccount(accessToken, domain) - - val intent = Intent(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - finish() - overridePendingTransition(R.anim.explode, R.anim.explode) - } - - companion object { - private const val TAG = "LoginActivity" // logging tag - private const val OAUTH_SCOPES = "read write follow" - private const val LOGIN_MODE = "LOGIN_MODE" - private const val DOMAIN = "domain" - private const val CLIENT_ID = "clientId" - private const val CLIENT_SECRET = "clientSecret" - - @JvmStatic - fun getIntent(context: Context, mode: Boolean): Intent { - val loginIntent = Intent(context, LoginActivity::class.java) - loginIntent.putExtra(LOGIN_MODE, mode) - return loginIntent - } - - /** Make sure the user-entered text is just a fully-qualified domain name. */ - private fun canonicalizeDomain(domain: String): String { - // Strip any schemes out. - var s = domain.replaceFirst("http://", "") - s = s.replaceFirst("https://", "") - // If a username was included (e.g. username@example.com), just take what's after the '@'. - val at = s.lastIndexOf('@') - if (at != -1) { - s = s.substring(at + 1) - } - return s.trim { it <= ' ' } - } - - /** - * Chain together the key-value pairs into a query string, for either appending to a URL or - * as the content of an HTTP request. - */ - private fun toQueryString(parameters: Map): String { - val s = StringBuilder() - var between = "" - for ((key, value) in parameters) { - s.append(between) - s.append(Uri.encode(key)) - s.append("=") - s.append(Uri.encode(value)) - between = "&" - } - return s.toString() - } - - private fun openInCustomTab(uri: Uri, context: Context): Boolean { - - val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) - val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) - val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) - - val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(toolbarColor) - .setNavigationBarColor(navigationbarColor) - .setNavigationBarDividerColor(navigationbarDividerColor) - .build() - - val customTabsIntent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .build() - - try { - customTabsIntent.launchUrl(context, uri) - } catch (e: ActivityNotFoundException) { - Log.w(TAG, "Activity was not found for intent $customTabsIntent") - return false - } - - return true - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 2290e7d7..5c7901c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -64,9 +64,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canH 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.login.LoginActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.preference.PreferencesActivity -import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity +import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.databinding.ActivityMainBinding import com.keylesspalace.tusky.db.AccountEntity @@ -325,9 +326,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje super.onPostCreate(savedInstanceState) if (intent != null) { - val statusUrl = intent.getStringExtra(STATUS_URL) - if (statusUrl != null) { - viewUrl(statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR) + val redirectUrl = intent.getStringExtra(REDIRECT_URL) + if (redirectUrl != null) { + viewUrl(redirectUrl, PostLookupFallbackBehavior.DISPLAY_ERROR) } } } @@ -454,10 +455,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }, primaryDrawerItem { - nameRes = R.string.action_access_scheduled_toot + nameRes = R.string.action_access_scheduled_posts iconRes = R.drawable.ic_access_time onClick = { - startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context)) + startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context)) } }, primaryDrawerItem { @@ -834,7 +835,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private const val TAG = "MainActivity" // logging tag private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 - const val STATUS_URL = "statusUrl" + const val REDIRECT_URL = "redirectUrl" } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt deleted file mode 100644 index 044349b9..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.keylesspalace.tusky - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import com.google.android.material.floatingactionbutton.FloatingActionButton -import com.keylesspalace.tusky.components.timeline.TimelineFragment -import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel -import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding -import com.keylesspalace.tusky.interfaces.ActionButtonActivity -import dagger.android.DispatchingAndroidInjector -import dagger.android.HasAndroidInjector -import javax.inject.Inject - -class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { - - @Inject - lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val binding = ActivityModalTimelineBinding.inflate(layoutInflater) - setContentView(binding.root) - - setSupportActionBar(binding.includedToolbar.toolbar) - supportActionBar?.apply { - title = getString(R.string.title_list_timeline) - setDisplayHomeAsUpEnabled(true) - setDisplayShowHomeEnabled(true) - } - - if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { - val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind - ?: TimelineViewModel.Kind.HOME - val argument = intent?.getStringExtra(ARG_ARG) - supportFragmentManager.beginTransaction() - .replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument)) - .commit() - } - } - - override fun getActionButton(): FloatingActionButton? = null - - override fun androidInjector() = dispatchingAndroidInjector - - companion object { - private const val ARG_KIND = "kind" - private const val ARG_ARG = "arg" - - @JvmStatic - fun newIntent( - context: Context, - kind: TimelineViewModel.Kind, - argument: String? - ): Intent { - val intent = Intent(context, ModalTimelineActivity::class.java) - intent.putExtra(ARG_KIND, kind) - intent.putExtra(ARG_ARG, argument) - return intent - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt index 1b7e2994..62f95162 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt @@ -15,14 +15,17 @@ package com.keylesspalace.tusky +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.keylesspalace.tusky.components.login.LoginActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import javax.inject.Inject +@SuppressLint("CustomSplashScreen") class SplashActivity : AppCompatActivity(), Injectable { @Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index ebe6c63e..82604022 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -31,9 +31,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector - private val kind: Kind - get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityStatuslistBinding.inflate(layoutInflater) @@ -41,10 +38,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { setSupportActionBar(binding.includedToolbar.toolbar) - val title = if (kind == Kind.FAVOURITES) { - R.string.title_favourites - } else { - R.string.title_bookmarks + val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) + val listId = intent.getStringExtra(EXTRA_LIST_ID) + val hashtag = intent.getStringExtra(EXTRA_HASHTAG) + + val title = when (kind) { + Kind.FAVOURITES -> getString(R.string.title_favourites) + Kind.BOOKMARKS -> getString(R.string.title_bookmarks) + Kind.TAG -> getString(R.string.title_tag).format(hashtag) + else -> getString(R.string.title_list_timeline) } supportActionBar?.run { @@ -53,9 +55,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { setDisplayShowHomeEnabled(true) } - supportFragmentManager.commit { - val fragment = TimelineFragment.newInstance(kind) - replace(R.id.fragment_container, fragment) + if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) { + supportFragmentManager.commit { + val fragment = if (kind == Kind.TAG) { + TimelineFragment.newHashtagInstance(listOf(hashtag!!)) + } else { + TimelineFragment.newInstance(kind, listId) + } + replace(R.id.fragmentContainer, fragment) + } } } @@ -64,17 +72,30 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { companion object { private const val EXTRA_KIND = "kind" + private const val EXTRA_LIST_ID = "id" + private const val EXTRA_HASHTAG = "tag" - @JvmStatic fun newFavouritesIntent(context: Context) = Intent(context, StatusListActivity::class.java).apply { putExtra(EXTRA_KIND, Kind.FAVOURITES.name) } - @JvmStatic fun newBookmarksIntent(context: Context) = Intent(context, StatusListActivity::class.java).apply { putExtra(EXTRA_KIND, Kind.BOOKMARKS.name) } + + fun newListIntent(context: Context, listId: String) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.LIST.name) + putExtra(EXTRA_LIST_ID, listId) + } + + @JvmStatic + fun newHashtagIntent(context: Context, hashtag: String) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.TAG.name) + putExtra(EXTRA_HASHTAG, hashtag) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 2598b5e2..64d29577 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -36,6 +36,7 @@ import android.view.MenuItem import android.view.View import android.webkit.MimeTypeMap import android.widget.Toast +import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle @@ -205,10 +206,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val request = DownloadManager.Request(Uri.parse(url)) - request.setDestinationInExternalPublicDir( - Environment.DIRECTORY_PICTURES, - getString(R.string.app_name) + "/" + filename - ) + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) downloadManager.enqueue(request) } @@ -255,11 +253,11 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } private fun shareFile(file: File, mimeType: String?) { - val sendIntent = Intent() - sendIntent.action = Intent.ACTION_SEND - sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) - sendIntent.type = mimeType - startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) + ShareCompat.IntentBuilder(this) + .setType(mimeType) + .addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) + .setChooserTitle(R.string.send_media_to) + .startChooser() } private var isCreating: Boolean = false diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java deleted file mode 100644 index 0071924b..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ /dev/null @@ -1,79 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; - -import com.keylesspalace.tusky.components.timeline.TimelineFragment; - -import java.util.Collections; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasAndroidInjector; - -public class ViewTagActivity extends BottomSheetActivity implements HasAndroidInjector { - - private static final String HASHTAG = "hashtag"; - - @Inject - public DispatchingAndroidInjector dispatchingAndroidInjector; - - public static Intent getIntent(Context context, String tag){ - Intent intent = new Intent(context,ViewTagActivity.class); - intent.putExtra(HASHTAG,tag); - return intent; - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_view_tag); - - String hashtag = getIntent().getStringExtra(HASHTAG); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar bar = getSupportActionBar(); - - if (bar != null) { - bar.setTitle(String.format(getString(R.string.title_tag), hashtag)); - bar.setDisplayHomeAsUpEnabled(true); - bar.setDisplayShowHomeEnabled(true); - } - - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - Fragment fragment = TimelineFragment.newHashtagInstance(Collections.singletonList(hashtag)); - fragmentTransaction.replace(R.id.fragment_container, fragment); - fragmentTransaction.commit(); - } - - @Override - public AndroidInjector androidInjector() { - return dispatchingAndroidInjector; - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index 88fb88cc..e45b783a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -111,7 +111,7 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_open_in_web: { - LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this); + openLink(getIntent().getStringExtra(URL_EXTRA)); return true; } case R.id.action_reveal: { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt index 320f8126..366dae7f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt @@ -18,7 +18,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.removeDuplicates @@ -28,7 +28,7 @@ abstract class AccountAdapter internal constructo protected val animateAvatar: Boolean, protected val animateEmojis: Boolean ) : RecyclerView.Adapter() { - var accountList = mutableListOf() + var accountList = mutableListOf() private var bottomLoading: Boolean = false override fun getItemCount(): Int { @@ -73,12 +73,12 @@ abstract class AccountAdapter internal constructo } } - fun update(newAccounts: List) { + fun update(newAccounts: List) { accountList = removeDuplicates(newAccounts) notifyDataSetChanged() } - fun addItems(newAccounts: List) { + fun addItems(newAccounts: List) { val end = accountList.size val last = accountList[end - 1] if (newAccounts.none { it.id == last.id }) { @@ -100,7 +100,7 @@ abstract class AccountAdapter internal constructo } } - fun removeItem(position: Int): Account? { + fun removeItem(position: Int): TimelineAccount? { if (position < 0 || position >= accountList.size) { return null } @@ -109,7 +109,7 @@ abstract class AccountAdapter internal constructo return account } - fun addItem(account: Account, position: Int) { + fun addItem(account: TimelineAccount, position: Int) { if (position < 0 || position > accountList.size) { return } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java index 559426e3..f4824389 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.util.CustomEmojiHelper; @@ -33,9 +33,9 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); } - public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { + public void setupWithAccount(TimelineAccount account, boolean animateAvatar, boolean animateEmojis) { accountId = account.getId(); - String format = username.getContext().getString(R.string.status_username_format); + String format = username.getContext().getString(R.string.post_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt index 33a23605..859807e9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt @@ -22,7 +22,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.loadAvatar @@ -55,11 +55,11 @@ class BlocksAdapter( private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock) private var id: String? = null - fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { + fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) { id = account.id val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis) displayName.text = emojifiedName - val format = username.context.getString(R.string.status_username_format) + val format = username.context.getString(R.string.post_username_format) val formattedUsername = String.format(format, account.username) username.text = formattedUsername val avatarRadius = avatar.context.resources diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index 2be8b762..38b301b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -22,7 +22,7 @@ import android.text.style.StyleSpan import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.loadAvatar @@ -34,7 +34,7 @@ class FollowRequestViewHolder( private val showHeader: Boolean ) : RecyclerView.ViewHolder(binding.root) { - fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { + fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) { val wrappedName = account.name.unicodeWrap() val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) binding.displayNameTextView.text = emojifiedName @@ -45,7 +45,7 @@ class FollowRequestViewHolder( }.emojify(account.emojis, itemView, animateEmojis) } binding.notificationTextView.visible(showHeader) - val format = itemView.context.getString(R.string.status_username_format) + val format = itemView.context.getString(R.string.post_username_format) val formattedUsername = String.format(format, account.username) binding.usernameTextView.text = formattedUsername val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt index 9fca33e8..41bb286f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt @@ -9,7 +9,7 @@ import android.widget.TextView import androidx.core.view.ViewCompat import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.loadAvatar @@ -69,7 +69,7 @@ class MutesAdapter( private var notifications = false fun setupWithAccount( - account: Account, + account: TimelineAccount, mutingNotifications: Boolean?, animateAvatar: Boolean, animateEmojis: Boolean @@ -77,7 +77,7 @@ class MutesAdapter( id = account.id val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis) displayName.text = emojifiedName - val format = username.context.getString(R.string.status_username_format) + val format = username.context.getString(R.string.post_username_format) val formattedUsername = String.format(format, account.username) username.text = formattedUsername val avatarRadius = avatar.context.resources diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 5613d139..8f022909 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -40,10 +40,10 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; -import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; @@ -335,7 +335,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.statusDisplayOptions = statusDisplayOptions; } - void setMessage(Account account) { + void setMessage(TimelineAccount account) { Context context = message.getContext(); String format = context.getString(R.string.notification_follow_format); @@ -346,7 +346,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { ); message.setText(emojifiedMessage); - String username = context.getString(R.string.status_username_format, account.getUsername()); + String username = context.getString(R.string.post_username_format, account.getUsername()); usernameView.setText(username); CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify( @@ -440,7 +440,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private void setUsername(String name) { Context context = username.getContext(); - String format = context.getString(R.string.status_username_format); + String format = context.getString(R.string.post_username_format); String usernameText = String.format(format, name); username.setText(usernameText); } @@ -538,9 +538,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); if (statusViewData.isExpanded()) { - contentWarningButton.setText(R.string.status_content_warning_show_less); + contentWarningButton.setText(R.string.post_content_warning_show_less); } else { - contentWarningButton.setText(R.string.status_content_warning_show_more); + contentWarningButton.setText(R.string.post_content_warning_show_more); } contentWarningButton.setOnClickListener(view -> { @@ -630,10 +630,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { contentCollapseButton.setVisibility(View.VISIBLE); if (statusViewData.isCollapsed()) { - contentCollapseButton.setText(R.string.status_content_warning_show_more); + contentCollapseButton.setText(R.string.post_content_warning_show_more); statusContent.setFilters(COLLAPSE_INPUT_FILTER); } else { - contentCollapseButton.setText(R.string.status_content_warning_show_less); + contentCollapseButton.setText(R.string.post_content_warning_show_less); statusContent.setFilters(NO_INPUT_FILTER); } } else { @@ -644,7 +644,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { CharSequence emojifiedText = CustomEmojiHelper.emojify( content, emojis, statusContent, statusDisplayOptions.animateEmojis() ); - LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener); + LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), statusViewData.getActionable().getTags(), listener); CharSequence emojifiedContentWarning; if (statusViewData.getSpoilerText() != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index f45667eb..1239ea71 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.HashTag; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CardViewMode; @@ -190,7 +191,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected void setUsername(String name) { Context context = username.getContext(); - String usernameText = context.getString(R.string.status_username_format, name); + String usernameText = context.getString(R.string.post_username_format, name); username.setText(usernameText); } @@ -202,6 +203,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { @NonNull Spanned content, @Nullable String spoilerText, @Nullable List mentions, + @Nullable List tags, @NonNull List emojis, @Nullable PollViewData poll, @NonNull StatusDisplayOptions statusDisplayOptions, @@ -222,21 +224,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } setContentWarningButtonText(!expanded); - this.setTextVisible(sensitive, !expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); + this.setTextVisible(sensitive, !expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener); }); - this.setTextVisible(sensitive, expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); + this.setTextVisible(sensitive, expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener); } else { contentWarningDescription.setVisibility(View.GONE); contentWarningButton.setVisibility(View.GONE); - this.setTextVisible(sensitive, true, content, mentions, emojis, poll, statusDisplayOptions, listener); + this.setTextVisible(sensitive, true, content, mentions, tags, emojis, poll, statusDisplayOptions, listener); } } private void setContentWarningButtonText(boolean expanded) { if (expanded) { - contentWarningButton.setText(R.string.status_content_warning_show_less); + contentWarningButton.setText(R.string.post_content_warning_show_less); } else { - contentWarningButton.setText(R.string.status_content_warning_show_more); + contentWarningButton.setText(R.string.post_content_warning_show_more); } } @@ -244,13 +246,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { boolean expanded, Spanned content, List mentions, + List tags, List emojis, @Nullable PollViewData poll, StatusDisplayOptions statusDisplayOptions, final StatusActionListener listener) { if (expanded) { CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis()); - LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); + LinkHelper.setClickableText(this.content, emojifiedText, mentions, tags, listener); for (int i = 0; i < mediaLabels.length; ++i) { updateMediaLabel(i, sensitive, expanded); } @@ -505,9 +508,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } if (sensitive) { - sensitiveMediaWarning.setText(R.string.status_sensitive_media_title); + sensitiveMediaWarning.setText(R.string.post_sensitive_media_title); } else { - sensitiveMediaWarning.setText(R.string.status_media_hidden_title); + sensitiveMediaWarning.setText(R.string.post_media_hidden_title); } sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); @@ -552,7 +555,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) { Context context = itemView.getContext(); CharSequence label = (sensitive && !showingContent) ? - context.getString(R.string.status_sensitive_media_title) : + context.getString(R.string.post_sensitive_media_title) : mediaDescriptions[index]; mediaLabels[index].setText(label); } @@ -604,7 +607,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { duration = formatDuration(attachment.getMeta().getDuration()) + " "; } if (TextUtils.isEmpty(attachment.getDescription())) { - return duration + context.getString(R.string.description_status_media_no_description_placeholder); + return duration + context.getString(R.string.description_post_media_no_description_placeholder); } else { return duration + attachment.getDescription(); } @@ -739,7 +742,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { @Nullable Object payloads) { if (payloads == null) { Status actionable = status.getActionable(); - setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions); + setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions); setUsername(status.getUsername()); setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions); setIsReply(actionable.getInReplyToId() != null); @@ -779,7 +782,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility()); setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), - actionable.getMentions(), actionable.getEmojis(), + actionable.getMentions(), actionable.getTags(), actionable.getEmojis(), PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions, listener); @@ -823,9 +826,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions), getReblogDescription(context, status), status.getUsername(), - actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "", - actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "", - actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "", + actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "", + actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "", + actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "", getMediaDescription(context, status), getVisibilityDescription(context, actionable.getVisibility()), getFavsText(context, actionable.getFavouritesCount()), @@ -840,7 +843,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { Status reblog = status.getRebloggingStatus(); if (reblog != null) { return context - .getString(R.string.status_boosted_format, reblog.getAccount().getUsername()); + .getString(R.string.post_boosted_format, reblog.getAccount().getUsername()); } else { return ""; } @@ -857,20 +860,20 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { (builder, a) -> { if (a.getDescription() == null) { String placeholder = - context.getString(R.string.description_status_media_no_description_placeholder); + context.getString(R.string.description_post_media_no_description_placeholder); return builder.append(placeholder); } else { builder.append("; "); return builder.append(a.getDescription()); } }); - return context.getString(R.string.description_status_media, mediaDescriptions); + return context.getString(R.string.description_post_media, mediaDescriptions); } private static CharSequence getContentWarningDescription(Context context, @NonNull StatusViewData.Concrete status) { if (!TextUtils.isEmpty(status.getSpoilerText())) { - return context.getString(R.string.description_status_cw, status.getSpoilerText()); + return context.getString(R.string.description_post_cw, status.getSpoilerText()); } else { return ""; } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 68415989..b054aea9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -70,7 +70,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { if (reblogging == null) { hideStatusInfo(); } else { - String rebloggedByDisplayName = reblogging.getAccount().getDisplayName(); + String rebloggedByDisplayName = reblogging.getAccount().getName(); setRebloggedByDisplayName(rebloggedByDisplayName, reblogging.getAccount().getEmojis(), statusDisplayOptions); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); @@ -86,7 +86,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { final StatusDisplayOptions statusDisplayOptions) { Context context = statusInfo.getContext(); CharSequence wrappedName = StringUtils.unicodeWrap(name); - CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); + CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName); CharSequence emojifiedText = CustomEmojiHelper.emojify( boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis() ); @@ -118,10 +118,10 @@ public class StatusViewHolder extends StatusBaseViewHolder { contentCollapseButton.setVisibility(View.VISIBLE); if (status.isCollapsed()) { - contentCollapseButton.setText(R.string.status_content_warning_show_more); + contentCollapseButton.setText(R.string.post_content_warning_show_more); content.setFilters(COLLAPSE_INPUT_FILTER); } else { - contentCollapseButton.setText(R.string.status_content_warning_show_less); + contentCollapseButton.setText(R.string.post_content_warning_show_less); content.setFilters(NO_INPUT_FILTER); } } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 50f96b26..12cb4a69 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -3,9 +3,7 @@ package com.keylesspalace.tusky.appstore import com.google.gson.Gson import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase -import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class CacheUpdater @Inject constructor( @@ -47,12 +45,7 @@ class CacheUpdater @Inject constructor( this.disposable.dispose() } - fun clearForUser(accountId: Long) { - Single.fromCallable { - appDatabase.timelineDao().removeAllForAccount(accountId) - appDatabase.timelineDao().removeAllUsersForAccount(accountId) - } - .subscribeOn(Schedulers.io()) - .subscribe() + suspend fun clearForUser(accountId: Long) { + appDatabase.timelineDao().removeAll(accountId) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index ffcad8cb..d02c9cce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -55,27 +55,31 @@ import com.keylesspalace.tusky.AccountListActivity import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.EditProfileActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.ViewMediaActivity -import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.databinding.ActivityAccountBinding +import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.DefaultTextWatcher import com.keylesspalace.tusky.util.Error -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible @@ -230,7 +234,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountFragmentViewPager.adapter = adapter binding.accountFragmentViewPager.offscreenPageLimit = 2 - val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) + val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media)) TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position -> tab.text = pageTitles[position] @@ -402,12 +406,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun onAccountChanged(account: Account?) { loadedAccount = account ?: return - val usernameFormatted = getString(R.string.status_username_format, account.username) + val usernameFormatted = getString(R.string.post_username_format, account.username) binding.accountUsernameTextView.text = usernameFormatted binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) - LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this) + setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() @@ -473,7 +477,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } catch (e: IllegalStateException) { supportActionBar?.title = emojifiedName } - supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username) + supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username) } } @@ -490,7 +494,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } binding.accountMovedDisplayName.text = movedAccount.name - binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) + binding.accountMovedUsername.text = getString(R.string.post_username_format, movedAccount.username) val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) @@ -515,7 +519,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (account.isRemote()) { binding.accountRemoveView.show() binding.accountRemoveView.setOnClickListener { - LinkHelper.openLink(account.url, this) + openLink(account.url) } } } @@ -686,6 +690,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.account_toolbar, menu) + val openAsItem = menu.findItem(R.id.action_open_as) + val title = openAsText + if (title == null) { + openAsItem.isVisible = false + } else { + openAsItem.title = title + } + if (!viewModel.isSelf) { val block = menu.findItem(R.id.action_block) @@ -704,7 +716,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (loadedAccount != null) { val muteDomain = menu.findItem(R.id.action_mute_domain) - domain = LinkHelper.getDomain(loadedAccount?.url) + domain = getDomain(loadedAccount?.url) if (domain.isEmpty()) { // If we can't get the domain, there's no way we can mute it anyway... menu.removeItem(R.id.action_mute_domain) @@ -805,8 +817,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } override fun onViewTag(tag: String) { - val intent = Intent(this, ViewTagActivity::class.java) - intent.putExtra("hashtag", tag) + val intent = StatusListActivity.newHashtagIntent(this, tag) startActivityWithSlideInAnimation(intent) } @@ -824,11 +835,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI when (item.itemId) { R.id.action_open_in_web -> { // If the account isn't loaded yet, eat the input. - if (loadedAccount != null) { - LinkHelper.openLink(loadedAccount?.url, this) + if (loadedAccount?.url != null) { + openLink(loadedAccount!!.url) } return true } + R.id.action_open_as -> { + if (loadedAccount != null) { + showAccountChooserDialog( + item.title, false, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + openAsAccount(loadedAccount!!.url, account) + } + } + ) + } + } R.id.action_block -> { toggleBlock() return true diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt index 4f4e204c..093dbcfb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.Either -import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.createClickableText import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.setClickableText class AccountFieldAdapter( private val linkListener: LinkListener, @@ -54,7 +55,7 @@ class AccountFieldAdapter( val identityProof = proofOrField.asLeft() nameTextView.text = identityProof.provider - valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) + valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl) valueTextView.movementMethod = LinkMovementMethod.getInstance() @@ -65,7 +66,7 @@ class AccountFieldAdapter( nameTextView.text = emojifiedName val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) - LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener) + setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) if (field.verifiedAt != null) { valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt index 0d2dcc77..bf31019e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -37,9 +37,9 @@ import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.SquareImageView @@ -252,7 +252,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } } Attachment.Type.UNKNOWN -> { - LinkHelper.openLink(items[currentIndex].attachment.url, context) + context?.openLink(items[currentIndex].attachment.url) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index 27284faf..4b5e7aa5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -31,8 +31,8 @@ import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.EmojiSpan -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.setClickableText import java.lang.ref.WeakReference interface AnnouncementActionListener : LinkListener { @@ -62,7 +62,7 @@ class AnnouncementAdapter( val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis) - LinkHelper.setClickableText(text, emojifiedText, item.mentions, listener) + setClickableText(text, emojifiedText, item.mentions, item.tags, listener) // If wellbeing mode is enabled, announcement badge counts should not be shown. if (wellbeingEnabled) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 3a9956af..560d453b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -27,7 +27,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding @@ -152,22 +152,17 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, viewModel.removeReaction(announcementId, name) } - override fun onViewTag(tag: String?) { - val intent = Intent(this, ViewTagActivity::class.java) - intent.putExtra("hashtag", tag) + override fun onViewTag(tag: String) { + val intent = StatusListActivity.newHashtagIntent(this, tag) startActivityWithSlideInAnimation(intent) } - override fun onViewAccount(id: String?) { - if (id != null) { - viewAccount(id) - } + override fun onViewAccount(id: String) { + viewAccount(id) } - override fun onViewUrl(url: String?) { - if (url != null) { - viewUrl(url) - } + override fun onViewUrl(url: String) { + viewUrl(url) } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index fef8f932..d1ae0b9e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.keylesspalace.tusky.appstore.AnnouncementReadEvent import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.InstanceEntity @@ -57,19 +58,21 @@ class AnnouncementsViewModel @Inject constructor( .onErrorResumeNext { mastodonApi.getInstance() .map { Either.Right(it) } - }, - { emojis, either -> - either.asLeftOrNull()?.copy(emojiList = emojis) - ?: InstanceEntity( - accountManager.activeAccount?.domain!!, - emojis, - either.asRight().maxTootChars, - either.asRight().pollLimits?.maxOptions, - either.asRight().pollLimits?.maxOptionChars, - either.asRight().version - ) - } - ) + } + ) { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars, + either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions, + either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars, + either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration, + either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration, + either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, + either.asRight().version + ) + } .doOnSuccess { appDatabase.instanceDao().insertOrReplace(it) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index dbb45d30..2bb97356 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -16,7 +16,9 @@ package com.keylesspalace.tusky.components.compose import android.Manifest +import android.app.NotificationManager import android.app.ProgressDialog +import android.content.ClipData import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -45,8 +47,8 @@ import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider -import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.core.view.inputmethod.InputContentInfoCompat +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.preference.PreferenceManager @@ -105,7 +107,7 @@ class ComposeActivity : ComposeAutoCompleteAdapter.AutocompletionProvider, OnEmojiSelectedListener, Injectable, - InputConnectionCompat.OnCommitContentListener, + OnReceiveContentListener, ComposeScheduleView.OnTimeSetListener { @Inject @@ -122,6 +124,7 @@ class ComposeActivity : @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT + var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH private val viewModel: ComposeViewModel by viewModels { viewModelFactory } @@ -148,6 +151,18 @@ class ComposeActivity : public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1) + if (notificationId != -1) { + // ComposeActivity was opened from a notification, delete the notification + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notificationId) + } + + val accountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1) + if (accountId != -1L) { + accountManager.setActiveAccount(accountId) + } + val preferences = PreferenceManager.getDefaultSharedPreferences(this) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) if (theme == "black") { @@ -186,9 +201,9 @@ class ComposeActivity : viewModel.setup(composeOptions) setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) - val tootText = composeOptions?.tootText - if (!tootText.isNullOrEmpty()) { - binding.composeEditField.setText(tootText) + val statusContent = composeOptions?.content + if (!statusContent.isNullOrEmpty()) { + binding.composeEditField.setText(statusContent) } if (!composeOptions?.scheduledAt.isNullOrEmpty()) { @@ -221,26 +236,25 @@ class ComposeActivity : } } } - } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { + } - val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) - val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty() - val shareBody = if (!subject.isNullOrBlank() && subject !in text) { - subject + '\n' + text - } else { - text - } + val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) + val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty() + val shareBody = if (!subject.isNullOrBlank() && subject !in text) { + subject + '\n' + text + } else { + text + } - if (shareBody.isNotBlank()) { - val start = binding.composeEditField.selectionStart.coerceAtLeast(0) - val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) - val left = min(start, end) - val right = max(start, end) - binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) - // move edittext cursor to first when shareBody parsed - binding.composeEditField.text.insert(0, "\n") - binding.composeEditField.setSelection(0) - } + if (shareBody.isNotBlank()) { + val start = binding.composeEditField.selectionStart.coerceAtLeast(0) + val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) + val left = min(start, end) + val right = max(start, end) + binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + // move edittext cursor to first when shareBody parsed + binding.composeEditField.text.insert(0, "\n") + binding.composeEditField.setSelection(0) } } } @@ -281,7 +295,7 @@ class ComposeActivity : } private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { - binding.composeEditField.setOnCommitContentListener(this) + binding.composeEditField.setOnReceiveContentListener(this) binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } @@ -316,6 +330,7 @@ class ComposeActivity : withLifecycleContext { viewModel.instanceParams.observe { instanceData -> maximumTootCharacters = instanceData.maxChars + charactersReservedPerUrl = instanceData.charactersReservedPerUrl updateVisibleCharactersLeft() binding.composeScheduleButton.visible(instanceData.supportsScheduled) } @@ -654,7 +669,8 @@ class ComposeActivity : val instanceParams = viewModel.instanceParams.value!! showAddPollDialog( this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, viewModel::updatePoll + instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration, + viewModel::updatePoll ) } @@ -699,7 +715,9 @@ class ComposeActivity : val urlSpans = binding.composeEditField.urls if (urlSpans != null) { for (span in urlSpans) { - offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) + // it's expected that this will be negative + // when the url length is less than the reserved character count + offset += (span.url.length - charactersReservedPerUrl) } } var length = binding.composeEditField.length() - offset @@ -739,26 +757,18 @@ class ComposeActivity : } } - /** This is for the fancy keyboards which can insert images and stuff. */ - override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { - // Verify the returned content's type is of the correct MIME type - val supported = inputContentInfo.description.hasMimeType("image/*") - - if (supported) { - val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 - if (lacksPermission) { - try { - inputContentInfo.requestPermission() - } catch (e: Exception) { - Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) - return false + /** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */ + override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? { + if (contentInfo.clip.description.hasMimeType("image/*")) { + val split = contentInfo.partition { item: ClipData.Item -> item.uri != null } + split.first?.let { content -> + for (i in 0 until content.clip.itemCount) { + pickMedia(content.clip.getItemAt(i).uri) } } - pickMedia(inputContentInfo.contentUri, inputContentInfo) - return true + return split.second } - - return false + return contentInfo } private fun sendStatus() { @@ -781,12 +791,11 @@ class ComposeActivity : } viewModel.sendStatus(contentText, spoilerText).observe( - this, - { - finishingUploadDialog?.dismiss() - deleteDraftAndFinish() - } - ) + this + ) { + finishingUploadDialog?.dismiss() + deleteDraftAndFinish() + } } else { binding.composeEditField.error = getString(R.string.error_compose_character_limit) enableButtons(true) @@ -856,12 +865,9 @@ class ComposeActivity : viewModel.removeMediaFromQueue(item) } - private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) { + private fun pickMedia(uri: Uri) { withLifecycleContext { viewModel.pickMedia(uri).observe { exceptionOrItem -> - - contentInfoCompat?.releasePermission() - exceptionOrItem.asLeftOrNull()?.let { val errorId = when (it) { is VideoSizeException -> { @@ -1017,7 +1023,7 @@ class ComposeActivity : // Let's keep fields var until all consumers are Kotlin var scheduledTootId: String? = null, var draftId: Int? = null, - var tootText: String? = null, + var content: String? = null, var mediaUrls: List? = null, var mediaDescriptions: List? = null, var mentionedUsernames: Set? = null, @@ -1040,16 +1046,32 @@ class ComposeActivity : private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" + private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID" + private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID" private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" - // Mastodon only counts URLs as this long in terms of status character limits - @VisibleForTesting - const val MAXIMUM_URL_LENGTH = 23 - + /** + * @param options ComposeOptions to configure the ComposeActivity + * @param notificationId the id of the notification that starts the Activity + * @param accountId the id of the account to compose with, null for the current account + * @return an Intent to start the ComposeActivity + */ @JvmStatic - fun startIntent(context: Context, options: ComposeOptions): Intent { + @JvmOverloads + fun startIntent( + context: Context, + options: ComposeOptions, + notificationId: Int? = null, + accountId: Long? = null + ): Intent { return Intent(context, ComposeActivity::class.java).apply { putExtra(COMPOSE_OPTIONS_EXTRA, options) + if (notificationId != null) { + putExtra(NOTIFICATION_ID_EXTRA, notificationId) + } + if (accountId != null) { + putExtra(ACCOUNT_ID_EXTRA, accountId) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java index b2fa94c3..8a4f0ce1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.components.compose; import android.content.Context; -import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,9 +27,9 @@ import android.widget.TextView; import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.HashTag; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; @@ -144,9 +143,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter AccountResult accountResult = ((AccountResult) getItem(position)); if (accountResult != null) { - Account account = accountResult.account; + TimelineAccount account = accountResult.account; String formattedUsername = context.getString( - R.string.status_username_format, + R.string.post_username_format, account.getUsername() ); accountViewHolder.username.setText(formattedUsername); @@ -268,9 +267,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter } public final static class AccountResult extends AutocompleteResult { - private final Account account; + private final TimelineAccount account; - public AccountResult(Account account) { + public AccountResult(TimelineAccount account) { this.account = account; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 7391ab1c..66dacfb4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient -import com.keylesspalace.tusky.service.TootToSend +import com.keylesspalace.tusky.service.StatusToSend import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.VersionUtils @@ -79,6 +79,9 @@ class ComposeViewModel @Inject constructor( maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, + pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, + pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, + charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false ) } @@ -102,18 +105,20 @@ class ComposeViewModel @Inject constructor( init { Single.zip( - api.getCustomEmojis(), api.getInstance(), - { emojis, instance -> - InstanceEntity( - instance = accountManager.activeAccount?.domain!!, - emojiList = emojis, - maximumTootCharacters = instance.maxTootChars, - maxPollOptions = instance.pollLimits?.maxOptions, - maxPollOptionLength = instance.pollLimits?.maxOptionChars, - version = instance.version - ) - } - ) + api.getCustomEmojis(), api.getInstance() + ) { emojis, instance -> + InstanceEntity( + instance = accountManager.activeAccount?.domain!!, + emojiList = emojis, + maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars, + maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions, + maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars, + minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, + maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, + charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, + version = instance.version + ) + } .doOnSuccess { db.instanceDao().insertOrReplace(it) } @@ -185,7 +190,7 @@ class ComposeViewModel @Inject constructor( is UploadEvent.ProgressEvent -> item.copy(uploadPercent = event.percentage) is UploadEvent.FinishedEvent -> - item.copy(id = event.attachment.id, uploadPercent = -1) + item.copy(id = event.mediaId, uploadPercent = -1) } synchronized(media) { val mediaValue = media.value!! @@ -303,7 +308,7 @@ class ComposeViewModel @Inject constructor( mediaDescriptions.add(item.description ?: "") } - val tootToSend = TootToSend( + val tootToSend = StatusToSend( text = content, warningText = spoilerText, visibility = statusVisibility.value!!.serverString(), @@ -451,7 +456,7 @@ class ComposeViewModel @Inject constructor( draftId = composeOptions?.draftId ?: 0 scheduledTootId = composeOptions?.scheduledTootId - startingText = composeOptions?.tootText + startingText = composeOptions?.content val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { @@ -506,11 +511,19 @@ fun mutableLiveData(default: T) = MutableLiveData().apply { value = defau const val DEFAULT_CHARACTER_LIMIT = 500 private const val DEFAULT_MAX_OPTION_COUNT = 4 private const val DEFAULT_MAX_OPTION_LENGTH = 50 +private const val DEFAULT_MIN_POLL_DURATION = 300 +private const val DEFAULT_MAX_POLL_DURATION = 604800 + +// Mastodon only counts URLs as this long in terms of status character limits +const val DEFAULT_MAXIMUM_URL_LENGTH = 23 data class ComposeInstanceParams( val maxChars: Int, val pollMaxOptions: Int, val pollMaxLength: Int, + val pollMinDuration: Int, + val pollMaxDuration: Int, + val charactersReservedPerUrl: Int, val supportsScheduled: Boolean ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 44d1e8ed..0e3ac9e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.components.compose +import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Environment @@ -25,7 +26,6 @@ import androidx.core.net.toUri import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia -import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN @@ -38,6 +38,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.util.Date @@ -45,7 +46,7 @@ import javax.inject.Inject sealed class UploadEvent { data class ProgressEvent(val percentage: Int) : UploadEvent() - data class FinishedEvent(val attachment: Attachment) : UploadEvent() + data class FinishedEvent(val mediaId: String) : UploadEvent() } fun createNewImageFile(context: Context): File { @@ -84,36 +85,70 @@ class MediaUploader @Inject constructor( fun prepareMedia(inUri: Uri): Single { return Single.fromCallable { - var mediaSize = getMediaSize(contentResolver, inUri) + var mediaSize = MEDIA_SIZE_UNKNOWN var uri = inUri - val mimeType = contentResolver.getType(uri) - - val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp") + var mimeType: String? = null try { - contentResolver.openInputStream(inUri).use { input -> - if (input == null) { - Log.w(TAG, "Media input is null") - uri = inUri - return@use + when (inUri.scheme) { + ContentResolver.SCHEME_CONTENT -> { + + mimeType = contentResolver.getType(uri) + + val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp") + + contentResolver.openInputStream(inUri).use { input -> + if (input == null) { + Log.w(TAG, "Media input is null") + uri = inUri + return@use + } + val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) + FileOutputStream(file.absoluteFile).use { out -> + input.copyTo(out) + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + mediaSize = getMediaSize(contentResolver, uri) + } + } } - val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) - FileOutputStream(file.absoluteFile).use { out -> - input.copyTo(out) - uri = FileProvider.getUriForFile( - context, - BuildConfig.APPLICATION_ID + ".fileprovider", - file - ) - mediaSize = getMediaSize(contentResolver, uri) + ContentResolver.SCHEME_FILE -> { + val path = uri.path + if (path == null) { + Log.w(TAG, "empty uri path $uri") + throw CouldNotOpenFileException() + } + val inputFile = File(path) + val suffix = inputFile.name.substringAfterLast('.', "tmp") + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix) + val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir) + val input = FileInputStream(inputFile) + + FileOutputStream(file.absoluteFile).use { out -> + input.copyTo(out) + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + mediaSize = getMediaSize(contentResolver, uri) + } + } + else -> { + Log.w(TAG, "Unknown uri scheme $uri") + throw CouldNotOpenFileException() } } } catch (e: IOException) { Log.w(TAG, e) - uri = inUri + throw CouldNotOpenFileException() } if (mediaSize == MEDIA_SIZE_UNKNOWN) { - throw CouldNotOpenFileException() + Log.w(TAG, "Could not determine file size of upload") + throw MediaTypeException() } if (mimeType != null) { @@ -139,6 +174,7 @@ class MediaUploader @Inject constructor( } } } else { + Log.w(TAG, "Could not determine mime type of upload") throw MediaTypeException() } } @@ -183,8 +219,8 @@ class MediaUploader @Inject constructor( val uploadDisposable = mastodonApi.uploadMedia(body, description) .subscribe( - { attachment -> - emitter.onNext(UploadEvent.FinishedEvent(attachment)) + { result -> + emitter.onNext(UploadEvent.FinishedEvent(result.id)) emitter.onComplete() }, { e -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index 7a4f7389..a87b1b23 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -20,6 +20,7 @@ package com.keylesspalace.tusky.components.compose.dialog import android.content.Context import android.view.LayoutInflater import android.view.WindowManager +import android.widget.ArrayAdapter import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.DialogAddPollBinding @@ -30,6 +31,8 @@ fun showAddPollDialog( poll: NewPoll?, maxOptionCount: Int, maxOptionLength: Int, + minDuration: Int, + maxDuration: Int, onUpdatePoll: (NewPoll) -> Unit ) { @@ -57,6 +60,13 @@ fun showAddPollDialog( binding.pollChoices.adapter = adapter + var durations = context.resources.getIntArray(R.array.poll_duration_values).toList() + val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration } + binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply { + setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item) + } + durations = durations.filter { it in minDuration..maxDuration } + binding.addChoiceButton.setOnClickListener { if (adapter.itemCount < maxOptionCount) { adapter.addChoice() @@ -66,7 +76,7 @@ fun showAddPollDialog( } } - val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast { + val pollDurationId = durations.indexOfLast { it <= poll?.expiresIn ?: 0 } @@ -79,13 +89,10 @@ fun showAddPollDialog( button.setOnClickListener { val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition - val pollDuration = context.resources - .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] - onUpdatePoll( NewPoll( options = adapter.pollOptions, - expiresIn = pollDuration, + expiresIn = durations[selectedPollDurationId], multiple = binding.multipleChoicesCheckBox.isChecked ) ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt index a8403c95..dca696d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -22,6 +22,8 @@ import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView +import androidx.core.view.OnReceiveContentListener +import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.emoji.widget.EmojiEditTextHelper @@ -32,41 +34,33 @@ class EditTextTyped @JvmOverloads constructor( ) : AppCompatMultiAutoCompleteTextView(context, attributeSet) { - private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this) init { // fix a bug with autocomplete and some keyboards val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) inputType = newInputType - super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)) + super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener)) } - override fun setKeyListener(input: KeyListener) { - super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input)) + override fun setKeyListener(input: KeyListener?) { + if (input != null) { + super.setKeyListener(emojiEditTextHelper.getKeyListener(input)) + } else { + super.setKeyListener(input) + } } - fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) { - onCommitContentListener = listener + fun setOnReceiveContentListener(listener: OnReceiveContentListener) { + ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener) } override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { val connection = super.onCreateInputConnection(editorInfo) - return if (onCommitContentListener != null) { - EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) - getEmojiEditTextHelper().onCreateInputConnection( - InputConnectionCompat.createWrapper( - connection, editorInfo, - onCommitContentListener!! - ), - editorInfo - )!! - } else { - connection - } - } - - private fun getEmojiEditTextHelper(): EmojiEditTextHelper { - return emojiEditTextHelper + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) + return emojiEditTextHelper.onCreateInputConnection( + InputConnectionCompat.createWrapper(this, connection, editorInfo), + editorInfo + )!! } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 0a469822..88c9dbad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -16,17 +16,17 @@ package com.keylesspalace.tusky.components.conversation import android.text.Spanned -import android.text.SpannedString import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverters import com.keylesspalace.tusky.db.Converters -import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Conversation import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.util.shouldTrimStatus import java.util.Date @@ -47,17 +47,15 @@ data class ConversationAccountEntity( val avatar: String, val emojis: List ) { - fun toAccount(): Account { - return Account( + fun toAccount(): TimelineAccount { + return TimelineAccount( id = id, username = username, displayName = displayName, + url = "", avatar = avatar, emojis = emojis, - url = "", localUsername = "", - note = SpannedString(""), - header = "" ) } } @@ -79,6 +77,7 @@ data class ConversationStatusEntity( val spoilerText: String, val attachments: ArrayList, val mentions: List, + val tags: List?, val showingHiddenContent: Boolean, val expanded: Boolean, val collapsible: Boolean, @@ -98,7 +97,7 @@ data class ConversationStatusEntity( if (inReplyToId != other.inReplyToId) return false if (inReplyToAccountId != other.inReplyToAccountId) return false if (account != other.account) return false - if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings + if (content.toString() != other.content.toString()) return false if (createdAt != other.createdAt) return false if (emojis != other.emojis) return false if (favouritesCount != other.favouritesCount) return false @@ -107,6 +106,7 @@ data class ConversationStatusEntity( if (spoilerText != other.spoilerText) return false if (attachments != other.attachments) return false if (mentions != other.mentions) return false + if (tags != other.tags) return false if (showingHiddenContent != other.showingHiddenContent) return false if (expanded != other.expanded) return false if (collapsible != other.collapsible) return false @@ -123,7 +123,7 @@ data class ConversationStatusEntity( result = 31 * result + (inReplyToId?.hashCode() ?: 0) result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) result = 31 * result + account.hashCode() - result = 31 * result + content.hashCode() + result = 31 * result + content.toString().hashCode() result = 31 * result + createdAt.hashCode() result = 31 * result + emojis.hashCode() result = 31 * result + favouritesCount @@ -132,6 +132,7 @@ data class ConversationStatusEntity( result = 31 * result + spoilerText.hashCode() result = 31 * result + attachments.hashCode() result = 31 * result + mentions.hashCode() + result = 31 * result + tags.hashCode() result = 31 * result + showingHiddenContent.hashCode() result = 31 * result + expanded.hashCode() result = 31 * result + collapsible.hashCode() @@ -162,6 +163,7 @@ data class ConversationStatusEntity( visibility = Status.Visibility.DIRECT, attachments = attachments, mentions = mentions, + tags = tags, application = null, pinned = false, muted = muted, @@ -171,7 +173,7 @@ data class ConversationStatusEntity( } } -fun Account.toEntity() = +fun TimelineAccount.toEntity() = ConversationAccountEntity( id = id, username = username, @@ -197,6 +199,7 @@ fun Status.toEntity() = spoilerText = spoilerText, attachments = attachments, mentions = mentions, + tags = tags, showingHiddenContent = false, expanded = false, collapsible = shouldTrimStatus(content), diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 2d2f683d..436ba84e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -108,7 +108,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { statusDisplayOptions); setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), - status.getMentions(), status.getEmojis(), + status.getMentions(), status.getTags(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); setConversationName(conversation.getAccounts()); @@ -154,10 +154,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder { contentCollapseButton.setVisibility(View.VISIBLE); if (collapsed) { - contentCollapseButton.setText(R.string.status_content_warning_show_more); + contentCollapseButton.setText(R.string.post_content_warning_show_more); content.setFilters(COLLAPSE_INPUT_FILTER); } else { - contentCollapseButton.setText(R.string.status_content_warning_show_less); + contentCollapseButton.setText(R.string.post_content_warning_show_less); content.setFilters(NO_INPUT_FILTER); } } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 1e233d38..21c6fef6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.conversation -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -31,7 +30,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable @@ -103,7 +102,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res initSwipeToRefresh() - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { viewModel.conversationFlow.collectLatest { pagingData -> adapter.submitData(pagingData) } @@ -233,8 +232,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onViewTag(tag: String) { - val intent = Intent(context, ViewTagActivity::class.java) - intent.putExtra("hashtag", tag) + val intent = StatusListActivity.newHashtagIntent(requireContext(), tag) startActivity(intent) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index ce004801..e580f554 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -90,14 +90,14 @@ class DraftsActivity : BaseActivity(), DraftActionListener { if (draft.inReplyToId != null) { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED - viewModel.getToot(draft.inReplyToId) + viewModel.getStatus(draft.inReplyToId) .observeOn(AndroidSchedulers.mainThread()) .autoDispose(from(this)) .subscribe( { status -> val composeOptions = ComposeActivity.ComposeOptions( draftId = draft.id, - tootText = draft.content, + content = draft.content, contentWarning = draft.contentWarning, inReplyToId = draft.inReplyToId, replyingStatusContent = status.content.toString(), @@ -121,7 +121,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { if (throwable is HttpException && throwable.code() == 404) { // the original status to which a reply was drafted has been deleted // let's open the ComposeActivity without reply information - Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show() + Toast.makeText(this, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() openDraftWithoutReply(draft) } else { Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) @@ -137,7 +137,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { private fun openDraftWithoutReply(draft: DraftEntity) { val composeOptions = ComposeActivity.ComposeOptions( draftId = draft.id, - tootText = draft.content, + content = draft.content, contentWarning = draft.contentWarning, draftAttachments = draft.attachments, poll = draft.poll, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index 78853d1e..0c370222 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -60,8 +60,8 @@ class DraftsViewModel @Inject constructor( } } - fun getToot(tootId: String): Single { - return api.status(tootId) + fun getStatus(statusId: String): Single { + return api.status(statusId) } override fun onCleared() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt new file mode 100644 index 00000000..d8a52a27 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -0,0 +1,309 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.login + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityLoginBinding +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.AppCredentials +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.rickRoll +import com.keylesspalace.tusky.util.shouldRickRoll +import com.keylesspalace.tusky.util.viewBinding +import kotlinx.coroutines.launch +import okhttp3.HttpUrl +import javax.inject.Inject + +/** Main login page, the first thing that users see. Has prompt for instance and login button. */ +class LoginActivity : BaseActivity(), Injectable { + + @Inject + lateinit var mastodonApi: MastodonApi + + private val binding by viewBinding(ActivityLoginBinding::inflate) + + private lateinit var preferences: SharedPreferences + + private val oauthRedirectUri: String + get() { + val scheme = getString(R.string.oauth_scheme) + val host = BuildConfig.APPLICATION_ID + return "$scheme://$host/" + } + + private val doWebViewAuth = registerForActivityResult(OauthLogin()) { result -> + when (result) { + is LoginResult.Ok -> lifecycleScope.launch { + fetchOauthToken(result.code) + } + is LoginResult.Err -> { + // Authorization failed. Put the error response where the user can read it and they + // can try again. + setLoading(false) + binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) + Log.e( + TAG, + "%s %s".format( + getString(R.string.error_authorization_denied), + result.errorMessage + ) + ) + } + is LoginResult.Cancel -> { + setLoading(false) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + if (savedInstanceState == null && + BuildConfig.CUSTOM_INSTANCE.isNotBlank() && + !isAdditionalLogin() + ) { + binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) + binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) + } + + if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { + Glide.with(binding.loginLogo) + .load(BuildConfig.CUSTOM_LOGO_URL) + .placeholder(null) + .into(binding.loginLogo) + } + + preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE + ) + + binding.loginButton.setOnClickListener { onButtonClick() } + binding.registerButton.setOnClickListener { onRegisterClick() } + + binding.whatsAnInstanceTextView.setOnClickListener { + val dialog = AlertDialog.Builder(this) + .setMessage(R.string.dialog_whats_an_instance) + .setPositiveButton(R.string.action_close, null) + .show() + val textView = dialog.findViewById(android.R.id.message) + textView?.movementMethod = LinkMovementMethod.getInstance() + } + + if (isAdditionalLogin()) { + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) + } else { + binding.toolbar.visibility = View.GONE + } + } + + override fun requiresLogin(): Boolean { + return false + } + + override fun finish() { + super.finish() + if (isAdditionalLogin()) { + overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) + } + } + + /** + * Handle registation of new account in the most basic way possible; open a URL + * in the system default browser. + */ + private fun onRegisterClick() { + binding.registerButton.isEnabled = false + + val openRegisterPage = Intent(android.content.Intent.ACTION_VIEW) + openRegisterPage.data = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL) + startActivity(openRegisterPage) + } + + /** + * Obtain the oauth client credentials for this app. This is only necessary the first time the + * app is run on a given server instance. So, after the first authentication, they are + * saved in SharedPreferences and every subsequent run they are simply fetched from there. + */ + private fun onButtonClick() { + binding.loginButton.isEnabled = false + binding.domainTextInputLayout.error = null + + val domain = canonicalizeDomain(binding.domainEditText.text.toString()) + + try { + HttpUrl.Builder().host(domain).scheme("https").build() + } catch (e: IllegalArgumentException) { + setLoading(false) + binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain) + return + } + + if (shouldRickRoll(this, domain)) { + rickRoll(this) + return + } + + setLoading(true) + + lifecycleScope.launch { + val credentials: AppCredentials = try { + mastodonApi.authenticateApp( + domain, getString(R.string.app_name), oauthRedirectUri, + OAUTH_SCOPES, getString(R.string.tusky_website) + ) + } catch (e: Exception) { + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = + getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, Log.getStackTraceString(e)) + return@launch + } + + // Before we open browser page we save the data. + // Even if we don't open other apps user may go to password manager or somewhere else + // and we will need to pick up the process where we left off. + // Alternatively we could pass it all as part of the intent and receive it back + // but it is a bit of a workaround. + preferences.edit() + .putString(DOMAIN, domain) + .putString(CLIENT_ID, credentials.clientId) + .putString(CLIENT_SECRET, credentials.clientSecret) + .apply() + + redirectUserToAuthorizeAndLogin(domain, credentials.clientId) + } + } + + private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { + // To authorize this app and log in it's necessary to redirect to the domain given, + // login there, and the server will redirect back to the app with its response. + val url = HttpUrl.Builder() + .scheme("https") + .host(domain) + .addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE) + .addQueryParameter("client_id", clientId) + .addQueryParameter("redirect_uri", oauthRedirectUri) + .addQueryParameter("response_type", "code") + .addQueryParameter("scope", OAUTH_SCOPES) + .build() + doWebViewAuth.launch(LoginData(url.toString().toUri(), oauthRedirectUri.toUri())) + } + + override fun onStart() { + super.onStart() + // first show or user cancelled login + setLoading(false) + } + + private suspend fun fetchOauthToken(code: String) { + /* restore variables from SharedPreferences */ + val domain = preferences.getNonNullString(DOMAIN, "") + val clientId = preferences.getNonNullString(CLIENT_ID, "") + val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "") + + setLoading(true) + + val accessToken = try { + mastodonApi.fetchOAuthToken( + domain, clientId, clientSecret, oauthRedirectUri, code, + "authorization_code" + ) + } catch (e: Exception) { + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_retrieving_oauth_token) + Log.e( + TAG, + "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message), + ) + return + } + + accountManager.addAccount(accessToken.accessToken, domain) + + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + overridePendingTransition(R.anim.explode, R.anim.explode) + } + + private fun setLoading(loadingState: Boolean) { + if (loadingState) { + binding.loginLoadingLayout.visibility = View.VISIBLE + binding.loginInputLayout.visibility = View.GONE + } else { + binding.loginLoadingLayout.visibility = View.GONE + binding.loginInputLayout.visibility = View.VISIBLE + binding.loginButton.isEnabled = true + } + } + + private fun isAdditionalLogin(): Boolean { + return intent.getBooleanExtra(LOGIN_MODE, false) + } + + companion object { + private const val TAG = "LoginActivity" // logging tag + private const val OAUTH_SCOPES = "read write follow" + private const val LOGIN_MODE = "LOGIN_MODE" + private const val DOMAIN = "domain" + private const val CLIENT_ID = "clientId" + private const val CLIENT_SECRET = "clientSecret" + + @JvmStatic + fun getIntent(context: Context, mode: Boolean): Intent { + val loginIntent = Intent(context, LoginActivity::class.java) + loginIntent.putExtra(LOGIN_MODE, mode) + return loginIntent + } + + /** Make sure the user-entered text is just a fully-qualified domain name. */ + private fun canonicalizeDomain(domain: String): String { + // Strip any schemes out. + var s = domain.replaceFirst("http://", "") + s = s.replaceFirst("https://", "") + // If a username was included (e.g. username@example.com), just take what's after the '@'. + val at = s.lastIndexOf('@') + if (at != -1) { + s = s.substring(at + 1) + } + return s.trim { it <= ' ' } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt new file mode 100644 index 00000000..01f6c3b0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -0,0 +1,162 @@ +package com.keylesspalace.tusky.components.login + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebStorage +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.result.contract.ActivityResultContract +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.databinding.LoginWebviewBinding +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.util.viewBinding +import kotlinx.parcelize.Parcelize + +/** Contract for starting [LoginWebViewActivity]. */ +class OauthLogin : ActivityResultContract() { + override fun createIntent(context: Context, input: LoginData): Intent { + val intent = Intent(context, LoginWebViewActivity::class.java) + intent.putExtra(DATA_EXTRA, input) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): LoginResult { + // Can happen automatically on up or back press + return if (resultCode == Activity.RESULT_CANCELED) { + LoginResult.Cancel + } else { + intent!!.getParcelableExtra(RESULT_EXTRA)!! + } + } + + companion object { + private const val RESULT_EXTRA = "result" + private const val DATA_EXTRA = "data" + + fun parseData(intent: Intent): LoginData { + return intent.getParcelableExtra(DATA_EXTRA)!! + } + + fun makeResultIntent(result: LoginResult): Intent { + val intent = Intent() + intent.putExtra(RESULT_EXTRA, result) + return intent + } + } +} + +@Parcelize +data class LoginData( + val url: Uri, + val oauthRedirectUrl: Uri, +) : Parcelable + +sealed class LoginResult : Parcelable { + @Parcelize + data class Ok(val code: String) : LoginResult() + + @Parcelize + data class Err(val errorMessage: String) : LoginResult() + + @Parcelize + object Cancel : LoginResult() +} + +/** Activity to do Oauth process using WebView. */ +class LoginWebViewActivity : BaseActivity(), Injectable { + private val binding by viewBinding(LoginWebviewBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val data = OauthLogin.parseData(intent) + + setContentView(binding.root) + + setSupportActionBar(binding.loginToolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) + + val webView = binding.loginWebView + webView.settings.allowContentAccess = false + webView.settings.allowFileAccess = false + webView.settings.databaseEnabled = false + webView.settings.displayZoomControls = false + webView.settings.javaScriptCanOpenWindowsAutomatically = false + // Javascript needs to be enabled because otherwise 2FA does not work in some instances + @SuppressLint("SetJavaScriptEnabled") + webView.settings.javaScriptEnabled = true + webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}" + + val oauthUrl = data.oauthRedirectUrl + + webView.webViewClient = object : WebViewClient() { + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError + ) { + Log.d("LoginWeb", "Failed to load ${data.url}: $error") + finish() + } + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + val url = request.url + return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) { + val error = url.getQueryParameter("error") + if (error != null) { + sendResult(LoginResult.Err(error)) + } else { + val code = url.getQueryParameter("code").orEmpty() + sendResult(LoginResult.Ok(code)) + } + true + } else { + false + } + } + } + webView.setBackgroundColor(Color.TRANSPARENT) + + if (savedInstanceState == null) { + webView.loadUrl(data.url.toString()) + } else { + webView.restoreState(savedInstanceState) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.loginWebView.saveState(outState) + } + + override fun onDestroy() { + if (isFinishing) { + // We don't want to keep user session in WebView, we just want our own accessToken + WebStorage.getInstance().deleteAllData() + CookieManager.getInstance().removeAllCookies(null) + } + super.onDestroy() + } + + override fun requiresLogin() = false + + private fun sendResult(result: LoginResult) { + setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result)) + finish() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 4b7d3dc9..6b9afce1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -24,7 +24,6 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Color; import android.os.Build; import android.provider.Settings; import android.text.TextUtils; @@ -46,9 +45,9 @@ import androidx.work.WorkRequest; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.FutureTarget; -import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.components.compose.ComposeActivity; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.entity.Notification; @@ -67,6 +66,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -88,8 +88,6 @@ public class NotificationHelper { public static final String REPLY_ACTION = "REPLY_ACTION"; - public static final String COMPOSE_ACTION = "COMPOSE_ACTION"; - public static final String KEY_REPLY = "KEY_REPLY"; public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID"; @@ -108,10 +106,6 @@ public class NotificationHelper { public static final String KEY_MENTIONS = "KEY_MENTIONS"; - public static final String KEY_CITED_TEXT = "KEY_CITED_TEXT"; - - public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL"; - /** * notification channels used on Android O+ **/ @@ -206,21 +200,24 @@ public class NotificationHelper { .setLabel(context.getString(R.string.label_quick_reply)) .build(); - PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account); NotificationCompat.Action quickReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + context.getString(R.string.action_quick_reply), + quickReplyPendingIntent) .addRemoteInput(replyRemoteInput) .build(); builder.addAction(quickReplyAction); - PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); + PendingIntent composeIntent = getStatusComposeIntent(context, body, account); NotificationCompat.Action composeAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_compose_shortcut), composePendingIntent) + context.getString(R.string.action_compose_shortcut), + composeIntent) + .setShowsUserInterface(true) .build(); builder.addAction(composeAction); @@ -237,7 +234,6 @@ public class NotificationHelper { } // Summary - // ======= final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true); if (currentNotifications.length() != 1) { @@ -275,7 +271,7 @@ public class NotificationHelper { summaryStackBuilder.addNextIntent(summaryResultIntent); PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000), - PendingIntent.FLAG_UPDATE_CURRENT); + pendingIntentFlags(false)); // we have to switch account here Intent eventResultIntent = new Intent(context, MainActivity.class); @@ -285,18 +281,18 @@ public class NotificationHelper { eventStackBuilder.addNextIntent(eventResultIntent); PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(), - PendingIntent.FLAG_UPDATE_CURRENT); + pendingIntentFlags(false)); Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class); deleteIntent.putExtra(ACCOUNT_ID, account.getId()); PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + pendingIntentFlags(false)); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body)) .setSmallIcon(R.drawable.ic_notify) .setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent) .setDeleteIntent(deletePendingIntent) - .setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.chinwag_green)) + .setColor(ContextCompat.getColor(context, R.color.notification_color)) .setGroup(account.getAccountId()) .setAutoCancel(true) .setShortcutId(Long.toString(account.getId())) @@ -307,11 +303,9 @@ public class NotificationHelper { return builder; } - private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) { + private static PendingIntent getStatusReplyIntent(Context context, Notification body, AccountEntity account) { Status status = body.getStatus(); - String citedLocalAuthor = status.getAccount().getLocalUsername(); - String citedText = status.getContent().toString(); String inReplyToId = status.getId(); Status actionableStatus = status.getActionableStatus(); Status.Visibility replyVisibility = actionableStatus.getVisibility(); @@ -326,9 +320,7 @@ public class NotificationHelper { mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) - .setAction(action) - .putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) - .putExtra(KEY_CITED_TEXT, citedText) + .setAction(REPLY_ACTION) .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) @@ -341,7 +333,50 @@ public class NotificationHelper { return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + pendingIntentFlags(true)); + } + + private static PendingIntent getStatusComposeIntent(Context context, Notification body, AccountEntity account) { + Status status = body.getStatus(); + + String citedLocalAuthor = status.getAccount().getLocalUsername(); + String citedText = status.getContent().toString(); + String inReplyToId = status.getId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + List mentions = actionableStatus.getMentions(); + Set mentionedUsernames = new LinkedHashSet<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + for (Status.Mention mention : mentions) { + String mentionedUsername = mention.getUsername(); + if (!mentionedUsername.equals(account.getUsername())) { + mentionedUsernames.add(mention.getUsername()); + } + } + + ComposeActivity.ComposeOptions composeOptions = new ComposeActivity.ComposeOptions(); + composeOptions.setInReplyToId(inReplyToId); + composeOptions.setReplyVisibility(replyVisibility); + composeOptions.setContentWarning(contentWarning); + composeOptions.setReplyingStatusAuthor(citedLocalAuthor); + composeOptions.setReplyingStatusContent(citedText); + composeOptions.setMentionedUsernames(mentionedUsernames); + composeOptions.setModifiedInitialState(true); + + Intent composeIntent = ComposeActivity.startIntent( + context, + composeOptions, + notificationId, + account.getId() + ); + + composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + return PendingIntent.getActivity(context.getApplicationContext(), + notificationId, + composeIntent, + pendingIntentFlags(false)); } public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { @@ -409,9 +444,7 @@ public class NotificationHelper { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - //noinspection ConstantConditions notificationManager.deleteNotificationChannelGroup(account.getIdentifier()); - } } @@ -421,7 +454,6 @@ public class NotificationHelper { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // used until Tusky 1.4 - //noinspection ConstantConditions notificationManager.deleteNotificationChannel(CHANNEL_MENTION); notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE); notificationManager.deleteNotificationChannel(CHANNEL_BOOST); @@ -440,7 +472,6 @@ public class NotificationHelper { // on Android >= O, notifications are enabled, if at least one channel is enabled NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - //noinspection ConstantConditions if (notificationManager.areNotificationsEnabled()) { for (NotificationChannel channel : notificationManager.getNotificationChannels()) { if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { @@ -491,7 +522,6 @@ public class NotificationHelper { accountManager.saveAccount(account); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - //noinspection ConstantConditions notificationManager.cancel((int) account.getId()); return true; }) @@ -511,7 +541,6 @@ public class NotificationHelper { // unknown notificationtype return false; } - //noinspection ConstantConditions NotificationChannel channel = notificationManager.getNotificationChannel(channelId); return channel.getImportance() > NotificationManager.IMPORTANCE_NONE; } @@ -674,4 +703,11 @@ public class NotificationHelper { return null; } + public static int pendingIntentFlags(boolean mutable) { + if (mutable) { + return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0); + } else { + return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index e793f17f..47cb37ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -15,6 +15,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceManager import com.keylesspalace.tusky.R import com.keylesspalace.tusky.SplashActivity +import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding import com.keylesspalace.tusky.util.EmojiCompatFont @@ -220,7 +221,7 @@ class EmojiPreference( context, 0x1f973, // This is the codepoint of the party face emoji :D launchIntent, - PendingIntent.FLAG_CANCEL_CURRENT + NotificationHelper.pendingIntentFlags(false) ) val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager mgr.set( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index ee79a624..a650f76b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -78,7 +78,7 @@ class PreferencesActivity : NotificationPreferencesFragment.newInstance() } TAB_FILTER_PREFERENCES -> { - setTitle(R.string.pref_title_status_tabs) + setTitle(R.string.pref_title_post_tabs) TabFilterPreferencesFragment.newInstance() } PROXY_PREFERENCES -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index 3aaca40f..61f86627 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -86,11 +86,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { listPreference { setDefaultValue("medium") - setEntries(R.array.status_text_size_names) - setEntryValues(R.array.status_text_size_values) + setEntries(R.array.post_text_size_names) + setEntryValues(R.array.post_text_size_values) key = PrefKeys.STATUS_TEXT_SIZE setSummaryProvider { entry } - setTitle(R.string.pref_status_text_size) + setTitle(R.string.pref_post_text_size) icon = makeIcon(GoogleMaterial.Icon.gmd_format_size) } @@ -138,6 +138,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { isSingleLineTitle = false } + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_CUSTOM_EMOJIS + setTitle(R.string.pref_title_animate_custom_emojis) + isSingleLineTitle = false + } + switchPreference { setDefaultValue(true) key = PrefKeys.USE_BLURHASH @@ -179,13 +186,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { setTitle(R.string.pref_title_enable_swipe_for_tabs) isSingleLineTitle = false } - - switchPreference { - setDefaultValue(false) - key = PrefKeys.ANIMATE_CUSTOM_EMOJIS - setTitle(R.string.pref_title_animate_custom_emojis) - isSingleLineTitle = false - } } preferenceCategory(R.string.pref_title_browser_settings) { @@ -199,7 +199,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { preferenceCategory(R.string.pref_title_timeline_filters) { preference { - setTitle(R.string.pref_title_status_tabs) + setTitle(R.string.pref_title_post_tabs) setOnPreferenceClickListener { activity?.let { activity -> val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES) @@ -280,23 +280,24 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { } private fun updateHttpProxySummary() { - val sharedPreferences = preferenceManager.sharedPreferences - val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false) - val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "") + preferenceManager.sharedPreferences?.let { sharedPreferences -> + val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false) + val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "") - try { - val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1") - .toInt() + try { + val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1") + .toInt() - if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) { - httpProxyPref?.summary = "$httpServer:$httpPort" - return + if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) { + httpProxyPref?.summary = "$httpServer:$httpPort" + return + } + } catch (e: NumberFormatException) { + // user has entered wrong port, fall back to empty summary } - } catch (e: NumberFormatException) { - // user has entered wrong port, fall back to empty summary - } - httpProxyPref?.summary = "" + httpProxyPref?.summary = "" + } } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 41486506..1b3b0de6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -23,9 +23,9 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusViewHelper import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER @@ -33,6 +33,8 @@ import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER import com.keylesspalace.tusky.util.TimestampUtils import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.setClickableMentions +import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.shouldTrimStatus import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.viewdata.toViewData @@ -96,7 +98,7 @@ class StatusViewHolder( ) if (status.spoilerText.isBlank()) { - setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) + setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler) binding.statusContentWarningButton.hide() binding.statusContentWarningDescription.hide() } else { @@ -110,35 +112,36 @@ class StatusViewHolder( val contentShown = viewState.isContentShow(status.id, true) binding.statusContentWarningDescription.invalidate() viewState.setContentShow(status.id, !contentShown) - setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler) + setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) } } - setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler) + setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler) } } } private fun setContentWarningButtonText(contentShown: Boolean) { if (contentShown) { - binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less) + binding.statusContentWarningButton.setText(R.string.post_content_warning_show_less) } else { - binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more) + binding.statusContentWarningButton.setText(R.string.post_content_warning_show_more) } } private fun setTextVisible( expanded: Boolean, content: Spanned, - mentions: List?, + mentions: List, + tags: List?, emojis: List, listener: LinkListener ) { if (expanded) { val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis) - LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener) + setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener) } else { - LinkHelper.setClickableMentions(binding.statusContent, mentions, listener) + setClickableMentions(binding.statusContent, mentions, listener) } if (binding.statusContent.text.isNullOrBlank()) { binding.statusContent.hide() @@ -174,10 +177,10 @@ class StatusViewHolder( binding.buttonToggleContent.show() if (collapsed) { - binding.buttonToggleContent.setText(R.string.status_content_show_more) + binding.buttonToggleContent.setText(R.string.post_content_show_more) binding.statusContent.filters = COLLAPSE_INPUT_FILTER } else { - binding.buttonToggleContent.setText(R.string.status_content_show_less) + binding.buttonToggleContent.setText(R.string.post_content_show_less) binding.statusContent.filters = NO_INPUT_FILTER } } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 65f58902..fbad8431 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -29,8 +29,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.ViewMediaActivity -import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -152,7 +152,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun showError() { if (snackbarErrorRetry?.isShown != true) { - snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_posts, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { adapter.retry() } @@ -180,9 +180,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) - override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag)) + override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag)) - override fun onViewUrl(url: String?) = viewModel.checkClickedUrl(url) + override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url) companion object { fun newInstance() = ReportStatusesFragment() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt similarity index 89% rename from app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt rename to app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt index 68cd715a..9636ce0a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt @@ -29,7 +29,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.ComposeActivity -import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding +import com.keylesspalace.tusky.databinding.ActivityScheduledStatusBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject -class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { +class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -48,19 +48,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec @Inject lateinit var eventHub: EventHub - private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } + private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory } - private val adapter = ScheduledTootAdapter(this) + private val adapter = ScheduledStatusAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityScheduledTootBinding.inflate(layoutInflater) + val binding = ActivityScheduledStatusBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { - title = getString(R.string.title_scheduled_toot) + title = getString(R.string.title_scheduled_posts) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } @@ -94,7 +94,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec if (loadState.refresh is LoadState.NotLoading) { binding.progressBar.hide() if (adapter.itemCount == 0) { - binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_posts) binding.errorMessageView.show() } else { binding.errorMessageView.hide() @@ -121,7 +121,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec this, ComposeActivity.ComposeOptions( scheduledTootId = item.id, - tootText = item.params.text, + content = item.params.text, contentWarning = item.params.spoilerText, mediaAttachments = item.mediaAttachments, inReplyToId = item.params.inReplyToId, @@ -138,6 +138,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } companion object { - fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java) + fun newIntent(context: Context) = Intent(context, ScheduledStatusActivity::class.java) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt similarity index 84% rename from app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt rename to app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt index 75b83e5d..ec8110de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt @@ -20,18 +20,18 @@ import android.view.View import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding +import com.keylesspalace.tusky.databinding.ItemScheduledStatusBinding import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.util.BindingHolder -interface ScheduledTootActionListener { +interface ScheduledStatusActionListener { fun edit(item: ScheduledStatus) fun delete(item: ScheduledStatus) } -class ScheduledTootAdapter( - val listener: ScheduledTootActionListener -) : PagingDataAdapter>( +class ScheduledStatusAdapter( + val listener: ScheduledStatusActionListener +) : PagingDataAdapter>( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { return oldItem.id == newItem.id @@ -43,12 +43,12 @@ class ScheduledTootAdapter( } ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { - val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) return BindingHolder(binding) } - override fun onBindViewHolder(holder: BindingHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { getItem(position)?.let { item -> holder.binding.edit.isEnabled = true holder.binding.delete.isEnabled = true diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt similarity index 76% rename from app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt rename to app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt index c4994cef..c9af661e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt @@ -22,16 +22,16 @@ import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.rx3.await -class ScheduledTootPagingSourceFactory( +class ScheduledStatusPagingSourceFactory( private val mastodonApi: MastodonApi -) : () -> ScheduledTootPagingSource { +) : () -> ScheduledStatusPagingSource { private val scheduledTootsCache = mutableListOf() - private var pagingSource: ScheduledTootPagingSource? = null + private var pagingSource: ScheduledStatusPagingSource? = null - override fun invoke(): ScheduledTootPagingSource { - return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also { + override fun invoke(): ScheduledStatusPagingSource { + return ScheduledStatusPagingSource(mastodonApi, scheduledTootsCache).also { pagingSource = it } } @@ -42,9 +42,9 @@ class ScheduledTootPagingSourceFactory( } } -class ScheduledTootPagingSource( +class ScheduledStatusPagingSource( private val mastodonApi: MastodonApi, - private val scheduledTootsCache: MutableList + private val scheduledStatusesCache: MutableList ) : PagingSource() { override fun getRefreshKey(state: PagingState): String? { @@ -52,11 +52,11 @@ class ScheduledTootPagingSource( } override suspend fun load(params: LoadParams): LoadResult { - return if (params is LoadParams.Refresh && scheduledTootsCache.isNotEmpty()) { + return if (params is LoadParams.Refresh && scheduledStatusesCache.isNotEmpty()) { LoadResult.Page( - data = scheduledTootsCache, + data = scheduledStatusesCache, prevKey = null, - nextKey = scheduledTootsCache.lastOrNull()?.id + nextKey = scheduledStatusesCache.lastOrNull()?.id ) } else { try { @@ -71,7 +71,7 @@ class ScheduledTootPagingSource( nextKey = result.lastOrNull()?.id ) } catch (e: Exception) { - Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e) + Log.w("ScheduledStatuses", "Error loading scheduled statuses", e) LoadResult.Error(e) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt similarity index 93% rename from app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt rename to app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt index 14f012ba..cd3e5ac0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt @@ -28,12 +28,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import javax.inject.Inject -class ScheduledTootViewModel @Inject constructor( +class ScheduledStatusViewModel @Inject constructor( val mastodonApi: MastodonApi, val eventHub: EventHub ) : ViewModel() { - private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi) + private val pagingSourceFactory = ScheduledStatusPagingSourceFactory(mastodonApi) val data = Pager( config = PagingConfig(pageSize = 20, initialLoadSize = 20), diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index 2326bf17..d833b432 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -86,7 +86,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { private fun getPageTitle(position: Int): CharSequence { return when (position) { - 0 -> getString(R.string.title_statuses) + 0 -> getString(R.string.title_posts) 1 -> getString(R.string.title_accounts) 2 -> getString(R.string.title_hashtags_dialog) else -> throw IllegalArgumentException("Unknown page index: $position") diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index e87c0c41..065aa040 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -53,17 +53,15 @@ class SearchViewModel @Inject constructor( val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false - private val loadedStatuses: MutableList> = mutableListOf() + private val loadedStatuses: MutableList = mutableListOf() private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) { it.statuses.map { status -> - val statusViewData = status.toViewData( + status.toViewData( isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, isExpanded = alwaysOpenSpoiler, isCollapsed = true ) - - Pair(status, statusViewData) }.apply { loadedStatuses.addAll(this) } @@ -100,11 +98,11 @@ class SearchViewModel @Inject constructor( hashtagsPagingSourceFactory.newSearch(query) } - fun removeItem(status: Pair) { - timelineCases.delete(status.first.id) + fun removeItem(statusViewData: StatusViewData.Concrete) { + timelineCases.delete(statusViewData.id) .subscribe( { - if (loadedStatuses.remove(status)) + if (loadedStatuses.remove(statusViewData)) statusesPagingSourceFactory.invalidate() }, { err -> @@ -114,82 +112,81 @@ class SearchViewModel @Inject constructor( .autoDispose() } - fun expandedChange(status: Pair, expanded: Boolean) { - val idx = loadedStatuses.indexOf(status) + fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) { + val idx = loadedStatuses.indexOf(statusViewData) if (idx >= 0) { - loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded)) + loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded) statusesPagingSourceFactory.invalidate() } } - fun reblog(status: Pair, reblog: Boolean) { - timelineCases.reblog(status.first.id, reblog) + fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) { + timelineCases.reblog(statusViewData.id, reblog) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { setRebloggedForStatus(status, reblog) }, - { t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) } + { setRebloggedForStatus(statusViewData, reblog) }, + { t -> Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t) } ) .autoDispose() } - private fun setRebloggedForStatus(status: Pair, reblog: Boolean) { - status.first.reblogged = reblog - status.first.reblog?.reblogged = reblog + private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) { + statusViewData.status.reblogged = reblog + statusViewData.status.reblog?.reblogged = reblog statusesPagingSourceFactory.invalidate() } - fun contentHiddenChange(status: Pair, isShowing: Boolean) { - val idx = loadedStatuses.indexOf(status) + fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) { + val idx = loadedStatuses.indexOf(statusViewData) if (idx >= 0) { - loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing)) + loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing) statusesPagingSourceFactory.invalidate() } } - fun collapsedChange(status: Pair, collapsed: Boolean) { - val idx = loadedStatuses.indexOf(status) + fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) { + val idx = loadedStatuses.indexOf(statusViewData) if (idx >= 0) { - loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed)) + loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed) statusesPagingSourceFactory.invalidate() } } - fun voteInPoll(status: Pair, choices: MutableList) { - val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices) - updateStatus(status, votedPoll) - timelineCases.voteInPoll(status.first.id, votedPoll.id, choices) + fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList) { + val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices) + updateStatus(statusViewData, votedPoll) + timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { newPoll -> updateStatus(status, newPoll) }, - { t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) } + { newPoll -> updateStatus(statusViewData, newPoll) }, + { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) } ) .autoDispose() } - private fun updateStatus(status: Pair, newPoll: Poll) { - val idx = loadedStatuses.indexOf(status) + private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) { + val idx = loadedStatuses.indexOf(statusViewData) if (idx >= 0) { - val newStatus = status.first.copy(poll = newPoll) - val newViewData = status.second.copy(status = newStatus) - loadedStatuses[idx] = Pair(newStatus, newViewData) + val newStatus = statusViewData.status.copy(poll = newPoll) + loadedStatuses[idx] = statusViewData.copy(status = newStatus) statusesPagingSourceFactory.invalidate() } } - fun favorite(status: Pair, isFavorited: Boolean) { - status.first.favourited = isFavorited + fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) { + statusViewData.status.favourited = isFavorited statusesPagingSourceFactory.invalidate() - timelineCases.favourite(status.first.id, isFavorited) - .onErrorReturnItem(status.first) + timelineCases.favourite(statusViewData.id, isFavorited) + .onErrorReturnItem(statusViewData.status) .subscribe() .autoDispose() } - fun bookmark(status: Pair, isBookmarked: Boolean) { - status.first.bookmarked = isBookmarked + fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) { + statusViewData.status.bookmarked = isBookmarked statusesPagingSourceFactory.invalidate() - timelineCases.bookmark(status.first.id, isBookmarked) - .onErrorReturnItem(status.first) + timelineCases.bookmark(statusViewData.id, isBookmarked) + .onErrorReturnItem(statusViewData.status) .subscribe() .autoDispose() } @@ -214,19 +211,15 @@ class SearchViewModel @Inject constructor( return timelineCases.delete(id) } - fun muteConversation(status: Pair, mute: Boolean) { - val idx = loadedStatuses.indexOf(status) + fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) { + val idx = loadedStatuses.indexOf(statusViewData) if (idx >= 0) { - val newStatus = status.first.copy(muted = mute) - val newPair = Pair( - newStatus, - status.second.copy(status = newStatus) - ) - loadedStatuses[idx] = newPair + val newStatus = statusViewData.status.copy(muted = mute) + loadedStatuses[idx] = statusViewData.copy(status = newStatus) statusesPagingSourceFactory.invalidate() } - timelineCases.muteConversation(status.first.id, mute) - .onErrorReturnItem(status.first) + timelineCases.muteConversation(statusViewData.id, mute) + .onErrorReturnItem(statusViewData.status) .subscribe() .autoDispose() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt index 71d58268..c4a3e826 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -21,11 +21,11 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.AccountViewHolder -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.LinkListener class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) : - PagingDataAdapter(ACCOUNT_COMPARATOR) { + PagingDataAdapter(ACCOUNT_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { val view = LayoutInflater.from(parent.context) @@ -44,11 +44,11 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val companion object { - val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean = - oldItem.deepEquals(newItem) + val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean = + oldItem == newItem - override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean = + override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean = oldItem.id == newItem.id } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt index d1ad3586..a7c4c90e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -21,7 +21,6 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.StatusViewHolder -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.viewdata.StatusViewData @@ -29,7 +28,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData class SearchStatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusListener: StatusActionListener -) : PagingDataAdapter, StatusViewHolder>(STATUS_COMPARATOR) { +) : PagingDataAdapter(STATUS_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { val view = LayoutInflater.from(parent.context) @@ -39,22 +38,18 @@ class SearchStatusesAdapter( override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { getItem(position)?.let { item -> - holder.setupWithStatus(item.second, statusListener, statusDisplayOptions) + holder.setupWithStatus(item, statusListener, statusDisplayOptions) } } - fun item(position: Int): Pair? { - return getItem(position) - } - companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback>() { - override fun areContentsTheSame(oldItem: Pair, newItem: Pair): Boolean = + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem == newItem - override fun areItemsTheSame(oldItem: Pair, newItem: Pair): Boolean = - oldItem.second.id == newItem.second.id + override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = + oldItem.id == newItem.id } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index d5e2a7ab..f59f84ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -19,12 +19,12 @@ import androidx.paging.PagingData import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.settings.PrefKeys import kotlinx.coroutines.flow.Flow -class SearchAccountsFragment : SearchFragment() { - override fun createAdapter(): PagingDataAdapter { +class SearchAccountsFragment : SearchFragment() { + override fun createAdapter(): PagingDataAdapter { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( @@ -34,7 +34,7 @@ class SearchAccountsFragment : SearchFragment() { ) } - override val data: Flow> + override val data: Flow> get() = viewModel.accountsFlow companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 8a4c47be..2d86f5f2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -15,7 +15,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.databinding.FragmentSearchBinding @@ -113,7 +113,7 @@ abstract class SearchFragment : override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) - override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag)) + override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag)) override fun onViewUrl(url: String) { bottomSheetActivity?.viewUrl(url) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 4b5f6717..23ff1b07 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -40,7 +40,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.autoDispose import com.keylesspalace.tusky.BaseActivity -import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -55,23 +54,23 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.Flow -class SearchStatusesFragment : SearchFragment>(), StatusActionListener { +class SearchStatusesFragment : SearchFragment(), StatusActionListener { - override val data: Flow>> + override val data: Flow> get() = viewModel.statusesFlow private val searchAdapter get() = super.adapter as SearchStatusesAdapter - override fun createAdapter(): PagingDataAdapter, *> { + override fun createAdapter(): PagingDataAdapter { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val statusDisplayOptions = StatusDisplayOptions( animateAvatars = preferences.getBoolean("animateGifAvatars", false), @@ -92,37 +91,37 @@ class SearchStatusesFragment : SearchFragment + searchAdapter.peek(position)?.status?.let { status -> reply(status) } } override fun onFavourite(favourite: Boolean, position: Int) { - searchAdapter.item(position)?.let { status -> + searchAdapter.peek(position)?.let { status -> viewModel.favorite(status, favourite) } } override fun onBookmark(bookmark: Boolean, position: Int) { - searchAdapter.item(position)?.let { status -> + searchAdapter.peek(position)?.let { status -> viewModel.bookmark(status, bookmark) } } override fun onMore(view: View, position: Int) { - searchAdapter.item(position)?.first?.let { + searchAdapter.peek(position)?.status?.let { more(it, view, position) } } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - searchAdapter.item(position)?.first?.actionableStatus?.let { actionable -> + searchAdapter.peek(position)?.status?.actionableStatus?.let { actionable -> when (actionable.attachments[attachmentIndex].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) @@ -143,27 +142,27 @@ class SearchStatusesFragment : SearchFragment { - LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context) + context?.openLink(actionable.attachments[attachmentIndex].url) } } } } override fun onViewThread(position: Int) { - searchAdapter.item(position)?.first?.let { status -> + searchAdapter.peek(position)?.status?.let { status -> val actionableStatus = status.actionableStatus bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url) } } override fun onOpenReblog(position: Int) { - searchAdapter.item(position)?.first?.let { status -> + searchAdapter.peek(position)?.status?.let { status -> bottomSheetActivity?.viewAccount(status.account.id) } } override fun onExpandedChange(expanded: Boolean, position: Int) { - searchAdapter.item(position)?.let { + searchAdapter.peek(position)?.let { viewModel.expandedChange(it, expanded) } } @@ -173,25 +172,25 @@ class SearchStatusesFragment : SearchFragment) { - searchAdapter.item(position)?.let { + searchAdapter.peek(position)?.let { viewModel.voteInPoll(it, choices) } } private fun removeItem(position: Int) { - searchAdapter.item(position)?.let { + searchAdapter.peek(position)?.let { viewModel.removeItem(it) } } override fun onReblog(reblog: Boolean, position: Int) { - searchAdapter.item(position)?.let { status -> + searchAdapter.peek(position)?.let { status -> viewModel.reblog(status, reblog) } } @@ -228,9 +227,6 @@ class SearchStatusesFragment : SearchFragment openAsItem.isVisible = false - 2 -> for (account in accounts) { - if (account !== viewModel.activeAccount) { - openAsTitle = String.format(getString(R.string.action_open_as), account.fullName) - break - } - } - else -> openAsTitle = String.format(getString(R.string.action_open_as), "…") + val openAsText = bottomSheetActivity?.openAsText + if (openAsText == null) { + openAsItem.isVisible = false + } else { + openAsItem.title = openAsText } - openAsItem.title = openAsTitle val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions) val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply { @@ -289,7 +280,7 @@ class SearchStatusesFragment : SearchFragment when (item.itemId) { - R.id.status_share_content -> { + R.id.post_share_content -> { val statusToShare: Status = status.actionableStatus val sendIntent = Intent() @@ -300,15 +291,15 @@ class SearchStatusesFragment : SearchFragment { + R.id.post_share_link -> { val sendIntent = Intent() sendIntent.action = Intent.ACTION_SEND sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl) sendIntent.type = "text/plain" - startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_link_to))) + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_link_to))) return@setOnMenuItemClickListener true } R.id.status_copy_link -> { @@ -325,7 +316,7 @@ class SearchStatusesFragment : SearchFragment { - searchAdapter.item(position)?.let { foundStatus -> + searchAdapter.peek(position)?.let { foundStatus -> viewModel.muteConversation(foundStatus, status.muted != true) } return@setOnMenuItemClickListener true @@ -396,21 +387,12 @@ class SearchStatusesFragment : SearchFragment viewModel.deleteStatus(id) removeItem(position) @@ -455,7 +437,7 @@ class SearchStatusesFragment : SearchFragment viewModel.deleteStatus(id) .observeOn(AndroidSchedulers.mainThread()) @@ -473,7 +455,7 @@ class SearchStatusesFragment : SearchFragment { - if (loadState.append 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) } @@ -218,43 +217,14 @@ class TimelineFragment : } }) - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { viewModel.statuses.collectLatest { pagingData -> adapter.submitData(pagingData) } } - } - private fun setupSwipeRefreshLayout() { - binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled - binding.swipeRefreshLayout.setOnRefreshListener(this) - binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - } - - private fun setupRecyclerView() { - binding.recyclerView.setAccessibilityDelegateCompat( - ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> - adapter.peek(pos) - } - ) - binding.recyclerView.setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - binding.recyclerView.layoutManager = layoutManager - val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) - binding.recyclerView.addItemDecoration(divider) - - // CWs are expanded without animation, buttons animate itself, we don't need it basically - (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - binding.recyclerView.adapter = adapter - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't - * guaranteed to be set until then. */ if (actionButtonPresent()) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) hideFab = preferences.getBoolean("fabHide", false) scrollListener = object : RecyclerView.OnScrollListener() { override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { @@ -276,23 +246,47 @@ class TimelineFragment : } } - if (!eventRegistered) { - eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { event -> - when (event) { - is PreferenceChangedEvent -> { - onPreferenceChanged(event.preferenceKey) - } - is StatusComposedEvent -> { - val status = event.status - handleStatusComposeEvent(status) - } + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event -> + when (event) { + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(status) } } - eventRegistered = true - } + } + } + + private fun setupSwipeRefreshLayout() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + + private fun setupRecyclerView() { + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> + if (pos in 0 until adapter.itemCount) { + adapter.peek(pos) + } else { + null + } + } + ) + binding.recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = layoutManager + val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + // CWs are expanded without animation, buttons animate itself, we don't need it basically + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter } override fun onRefresh() { @@ -407,7 +401,7 @@ class TimelineFragment : } private fun onPreferenceChanged(key: String) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) when (key) { PrefKeys.FAB_HIDE -> { hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) @@ -417,7 +411,7 @@ class TimelineFragment : val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled if (enabled != oldMediaPreviewEnabled) { adapter.mediaPreviewEnabled = enabled - adapter.notifyDataSetChanged() + adapter.notifyItemRangeChanged(0, adapter.itemCount) } } } @@ -463,7 +457,7 @@ class TimelineFragment : talkBackWasEnabled = a11yManager?.isEnabled == true Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") if (talkBackWasEnabled && !wasEnabled) { - adapter.notifyDataSetChanged() + adapter.notifyItemRangeChanged(0, adapter.itemCount) } startUpdateTimestamp() } @@ -474,14 +468,14 @@ class TimelineFragment : * Auto dispose observable on pause */ private fun startUpdateTimestamp() { - val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) if (!useAbsoluteTime) { Observable.interval(1, TimeUnit.MINUTES) .observeOn(AndroidSchedulers.mainThread()) .autoDispose(this, Lifecycle.Event.ON_PAUSE) .subscribe { - adapter.notifyDataSetChanged() + adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED)) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index e8b23c61..0ea0b958 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -41,6 +41,10 @@ class TimelinePagingAdapter( ) } + init { + stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_STATUS -> { @@ -110,7 +114,7 @@ class TimelinePagingAdapter( oldItem: StatusViewData, newItem: StatusViewData ): Boolean { - return oldItem.viewDataId == newItem.viewDataId + return oldItem.id == newItem.id } override fun areContentsTheSame( @@ -124,7 +128,7 @@ class TimelinePagingAdapter( oldItem: StatusViewData, newItem: StatusViewData ): Any? { - return if (oldItem === newItem) { + 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index 1f3810f9..252b9880 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -23,11 +23,12 @@ import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.db.TimelineAccountEntity import com.keylesspalace.tusky.db.TimelineStatusEntity import com.keylesspalace.tusky.db.TimelineStatusWithAccount -import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.util.shouldTrimStatus import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.viewdata.StatusViewData @@ -41,8 +42,9 @@ data class Placeholder( private val attachmentArrayListType = object : TypeToken>() {}.type private val emojisListType = object : TypeToken>() {}.type private val mentionListType = object : TypeToken>() {}.type +private val tagListType = object : TypeToken>() {}.type -fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { +fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { return TimelineAccountEntity( serverId = id, timelineUserId = accountId, @@ -56,25 +58,16 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { ) } -fun TimelineAccountEntity.toAccount(gson: Gson): Account { - return Account( +fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount { + return TimelineAccount( id = serverId, localUsername = localUsername, username = username, displayName = displayName, - note = SpannedString(""), url = url, avatar = avatar, - header = "", - locked = false, - followingCount = 0, - followersCount = 0, - statusesCount = 0, - source = null, bot = bot, - emojis = gson.fromJson(emojis, emojisListType), - fields = null, - moved = null + emojis = gson.fromJson(emojis, emojisListType) ) } @@ -99,6 +92,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { visibility = Status.Visibility.UNKNOWN, attachments = null, mentions = null, + tags = null, application = null, reblogServerId = null, reblogAccountId = null, @@ -138,6 +132,7 @@ fun Status.toEntity( visibility = actionableStatus.visibility, attachments = actionableStatus.attachments.let(gson::toJson), mentions = actionableStatus.mentions.let(gson::toJson), + tags = actionableStatus.tags.let(gson::toJson), application = actionableStatus.application.let(gson::toJson), reblogServerId = reblog?.id, reblogAccountId = reblog?.let { this.account.id }, @@ -157,6 +152,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { val attachments: ArrayList = gson.fromJson(status.attachments, attachmentArrayListType) ?: arrayListOf() val mentions: List = gson.fromJson(status.mentions, mentionListType) ?: emptyList() + val tags: List? = gson.fromJson(status.tags, tagListType) val application = gson.fromJson(status.application, Status.Application::class.java) val emojis: List = gson.fromJson(status.emojis, emojisListType) ?: emptyList() val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) @@ -183,6 +179,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { visibility = status.visibility, attachments = attachments, mentions = mentions, + tags = tags, application = application, pinned = false, muted = status.muted, @@ -211,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { visibility = status.visibility, attachments = ArrayList(), mentions = listOf(), + tags = listOf(), application = null, pinned = status.pinned, muted = status.muted, @@ -239,6 +237,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { visibility = status.visibility, attachments = attachments, mentions = mentions, + tags = tags, application = application, pinned = status.pinned, muted = status.muted, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt new file mode 100644 index 00000000..c8d95fd8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.components.timeline.util + +import retrofit2.HttpException +import java.io.IOException + +fun Throwable.isExpected() = this is IOException || this is HttpException + +inline fun ifExpected( + t: Throwable, + cb: () -> T +): T { + if (t.isExpected()) { + return cb() + } else { + throw t + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index e9d81e59..c4aa2c72 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -23,13 +23,13 @@ import androidx.room.withTransaction import com.google.gson.Gson import com.keylesspalace.tusky.components.timeline.Placeholder import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.TimelineStatusEntity import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.dec import kotlinx.coroutines.rx3.await import retrofit2.HttpException @@ -101,15 +101,22 @@ class CachedTimelineRemoteMediator( db.withTransaction { val overlappedStatuses = replaceStatusRange(statuses, state) - if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.isNotEmpty() && !dbEmpty) { + /* In case we loaded a whole page and there was no overlap with existing statuses, + we insert a placeholder because there might be even more unknown statuses */ + if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.size == state.config.pageSize && !dbEmpty) { + /* This overrides the last of the newly loaded statuses with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ timelineDao.insertStatus( - Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id) + Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id) ) } } return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) } catch (e: Exception) { - return MediatorResult.Error(e) + return ifExpected(e) { + MediatorResult.Error(e) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 1da37bf4..304b4e5a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -34,14 +34,13 @@ import com.keylesspalace.tusky.appstore.ReblogEvent import com.keylesspalace.tusky.components.timeline.Placeholder import com.keylesspalace.tusky.components.timeline.toEntity import com.keylesspalace.tusky.components.timeline.toViewData +import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.dec -import com.keylesspalace.tusky.util.inc import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map @@ -70,7 +69,14 @@ class CachedTimelineViewModel @Inject constructor( override val statuses = Pager( config = PagingConfig(pageSize = LOAD_AT_ONCE), remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson), - pagingSourceFactory = { db.timelineDao().getStatusesForAccount(accountManager.activeAccount!!.id) } + pagingSourceFactory = { + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { + EmptyTimelinePagingSource() + } else { + db.timelineDao().getStatuses(activeAccount.id) + } + } ).flow .map { pagingData -> pagingData.map { timelineStatus -> @@ -141,9 +147,11 @@ class CachedTimelineViewModel @Inject constructor( timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id)) - val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId) - - val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = 20).await() + val response = db.withTransaction { + val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId) + val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId) + api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE) + }.await() val statuses = response.body() if (!response.isSuccessful || statuses == null) { @@ -177,14 +185,21 @@ class CachedTimelineViewModel @Inject constructor( ) } - if (overlappedStatuses == 0 && statuses.isNotEmpty()) { + /* In case we loaded a whole page and there was no overlap with existing statuses, + we insert a placeholder because there might be even more unknown statuses */ + if (overlappedStatuses == 0 && statuses.size == LOAD_AT_ONCE) { + /* This overrides the last of the newly loaded statuses with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ timelineDao.insertStatus( - Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id) + Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id) ) } } } catch (e: Exception) { - loadMoreFailed(placeholderId, e) + ifExpected(e) { + loadMoreFailed(placeholderId, e) + } } } } @@ -214,10 +229,7 @@ class CachedTimelineViewModel @Inject constructor( override fun fullReload() { viewModelScope.launch { val activeAccount = accountManager.activeAccount!! - db.runInTransaction { - db.timelineDao().removeAllForAccount(activeAccount.id) - db.timelineDao().removeAllUsersForAccount(activeAccount.id) - } + db.timelineDao().removeAll(activeAccount.id) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt new file mode 100644 index 00000000..5fd13dfb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.components.timeline.viewmodel + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.db.TimelineStatusWithAccount + +class EmptyTimelinePagingSource : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null + + override suspend fun load(params: LoadParams): LoadResult = LoadResult.Page(emptyList(), null, null) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt index 114faa92..82cfd41d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -19,9 +19,9 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.util.HttpHeaderLink -import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData import retrofit2.HttpException @@ -92,7 +92,7 @@ class NetworkTimelineRemoteMediator( viewModel.statusData.addAll(0, data) if (insertPlaceholder) { - viewModel.statusData.add(statuses.size, StatusViewData.Placeholder(statuses.last().id.dec(), false)) + viewModel.statusData[statuses.size - 1] = StatusViewData.Placeholder(statuses.last().id, false) } } else { val linkHeader = statusResponse.headers()["Link"] @@ -107,7 +107,9 @@ class NetworkTimelineRemoteMediator( viewModel.currentSource?.invalidate() return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) } catch (e: Exception) { - return MediatorResult.Error(e) + return ifExpected(e) { + MediatorResult.Error(e) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 5815662d..f70fdcc8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -28,14 +28,16 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.FavoriteEvent import com.keylesspalace.tusky.appstore.PinEvent import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.LinkHelper -import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.getDomain +import com.keylesspalace.tusky.util.isLessThan +import com.keylesspalace.tusky.util.isLessThanOrEqual import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.flow.map @@ -43,6 +45,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import retrofit2.HttpException import retrofit2.Response +import java.io.IOException import javax.inject.Inject /** @@ -117,7 +120,7 @@ class NetworkTimelineViewModel @Inject constructor( override fun removeAllByInstance(instance: String) { statusData.removeAll { vd -> val status = vd.asStatusOrNull()?.status ?: return@removeAll false - LinkHelper.getDomain(status.account.url) == instance + getDomain(status.account.url) == instance } currentSource?.invalidate() } @@ -133,8 +136,14 @@ class NetworkTimelineViewModel @Inject constructor( override fun loadMore(placeholderId: String) { viewModelScope.launch { try { + val placeholderIndex = + statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId } + statusData[placeholderIndex] = StatusViewData.Placeholder(placeholderId, isLoading = true) + + val idAbovePlaceholder = statusData.getOrNull(placeholderIndex - 1)?.id + val statusResponse = fetchStatusesForKind( - fromId = placeholderId.inc(), + fromId = idAbovePlaceholder, uptoId = null, limit = 20 ) @@ -145,32 +154,53 @@ class NetworkTimelineViewModel @Inject constructor( return@launch } + statusData.removeAt(placeholderIndex) + val activeAccount = accountManager.activeAccount!! - - val data = statuses.map { status -> - val oldStatus = statusData.find { s -> - s.asStatusOrNull()?.id == status.id - }?.asStatusOrNull() - - val contentShowing = oldStatus?.isShowingContent ?: activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive - val expanded = oldStatus?.isExpanded ?: activeAccount.alwaysOpenSpoiler - val contentCollapsed = oldStatus?.isCollapsed ?: true - + val data: MutableList = statuses.map { status -> status.toViewData( - isShowingContent = contentShowing, - isExpanded = expanded, - isCollapsed = contentCollapsed + isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + isExpanded = activeAccount.alwaysOpenSpoiler, + isCollapsed = true ) + }.toMutableList() + + if (statuses.isNotEmpty()) { + val firstId = statuses.first().id + val lastId = statuses.last().id + val overlappedFrom = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThanOrEqual(firstId) ?: false } + val overlappedTo = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThan(lastId) ?: false } + + if (overlappedFrom < overlappedTo) { + data.mapIndexed { i, status -> i to statusData.firstOrNull { it.asStatusOrNull()?.id == status.id }?.asStatusOrNull() } + .filter { (_, oldStatus) -> oldStatus != null } + .forEach { (i, oldStatus) -> + data[i] = data[i].asStatusOrNull()!! + .copy( + isShowingContent = oldStatus!!.isShowingContent, + isExpanded = oldStatus.isExpanded, + isCollapsed = oldStatus.isCollapsed, + ) + } + + statusData.removeAll { status -> + when (status) { + is StatusViewData.Placeholder -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(firstId) + is StatusViewData.Concrete -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(firstId) + } + } + } else { + data[data.size - 1] = StatusViewData.Placeholder(statuses.last().id, isLoading = false) + } } - val index = - statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId } - statusData.removeAt(index) - statusData.addAll(index, data) + statusData.addAll(placeholderIndex, data) currentSource?.invalidate() } catch (e: Exception) { - loadMoreFailed(placeholderId, e) + ifExpected(e) { + loadMoreFailed(placeholderId, e) + } } } } @@ -210,10 +240,12 @@ class NetworkTimelineViewModel @Inject constructor( } override fun fullReload() { + nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id statusData.clear() currentSource?.invalidate() } + @Throws(IOException::class, HttpException::class) suspend fun fetchStatusesForKind( fromId: String?, uptoId: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index c7c95636..544d0818 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -33,6 +33,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.ReblogEvent import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.appstore.UnfollowEvent +import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Poll @@ -46,8 +47,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.asFlow import kotlinx.coroutines.rx3.await -import retrofit2.HttpException -import java.io.IOException abstract class TimelineViewModel( private val timelineCases: TimelineCases, @@ -291,19 +290,6 @@ abstract class TimelineViewModel( } } - private fun isExpectedRequestException(t: Exception) = t is IOException || t is HttpException - - private inline fun ifExpected( - t: Exception, - cb: () -> Unit - ) { - if (isExpectedRequestException(t)) { - cb() - } else { - throw t - } - } - companion object { private const val TAG = "TimelineVM" internal const val LOAD_AT_ONCE = 30 diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 50e13ca2..159a6f52 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -29,10 +29,9 @@ import java.io.File; /** * DB version & declare DAO */ - @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 28) + }, version = 31) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -457,4 +456,31 @@ public abstract class AppDatabase extends RoomDatabase { "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); } }; + + public static final Migration MIGRATION_28_29 = new Migration(28, 29) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_tags` TEXT"); + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `tags` TEXT"); + } + }; + + public static final Migration MIGRATION_29_30 = new Migration(29, 30) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `charactersReservedPerUrl` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `minPollDuration` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` INTEGER"); + } + }; + + public static final Migration MIGRATION_30_31 = new Migration(30, 31) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // no actual scheme change, but placeholder ids are now used differently so the cache needs to be cleared to avoid bugs + database.execSQL("DELETE FROM `TimelineAccountEntity`"); + database.execSQL("DELETE FROM `TimelineStatusEntity`"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index a59133de..c9daec0a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -27,6 +27,7 @@ import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.createTabDataFromId import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status @@ -119,6 +120,16 @@ class Converters @Inject constructor ( return gson.fromJson(mentionListJson, object : TypeToken>() {}.type) } + @TypeConverter + fun tagListToJson(tagArray: List?): String? { + return gson.toJson(tagArray) + } + + @TypeConverter + fun jsonToTagArray(tagListJson: String?): List? { + return gson.fromJson(tagListJson, object : TypeToken>() {}.type) + } + @TypeConverter fun dateToLong(date: Date): Long { return date.time diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt index ac4464f2..dd8e85d0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -28,5 +28,8 @@ data class InstanceEntity( val maximumTootCharacters: Int?, val maxPollOptions: Int?, val maxPollOptionLength: Int?, + val minPollDuration: Int?, + val maxPollDuration: Int?, + val charactersReservedPerUrl: Int?, val version: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 6952ca86..dd59f2a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -35,7 +35,7 @@ abstract class TimelineDao { SELECT s.serverId, s.url, s.timelineUserId, s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, -s.spoilerText, s.visibility, s.mentions, s.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.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', a.localUsername as 'a_localUsername', a.username as 'a_username', @@ -51,7 +51,7 @@ LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND WHERE s.timelineUserId = :account ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC""" ) - abstract fun getStatusesForAccount(account: Long): PagingSource + abstract fun getStatuses(account: Long): PagingSource @Query( """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND @@ -86,11 +86,20 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = ) abstract fun removeAllByUser(accountId: Long, userId: String) + /** + * Removes everything in the TimelineStatusEntity and TimelineAccountEntity tables for one user account + * @param accountId id of the account for which to clean tables + */ + suspend fun removeAll(accountId: Long) { + removeAllStatuses(accountId) + removeAllAccounts(accountId) + } + @Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId") - abstract fun removeAllForAccount(accountId: Long) + abstract suspend fun removeAllStatuses(accountId: Long) @Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId") - abstract fun removeAllUsersForAccount(accountId: Long) + abstract suspend fun removeAllAccounts(accountId: Long) @Query( """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId @@ -98,6 +107,16 @@ AND serverId = :statusId""" ) abstract fun delete(accountId: Long, statusId: String) + /** + * Cleans the TimelineStatusEntity and TimelineAccountEntity tables from old entries. + * @param accountId id of the account for which to clean tables + * @param limit how many statuses to keep + */ + suspend fun cleanup(accountId: Long, limit: Int) { + cleanupStatuses(accountId, limit) + cleanupAccounts(accountId) + } + /** * Cleans the TimelineStatusEntity table from old status entries. * @param accountId id of the account for which to clean statuses @@ -108,7 +127,7 @@ AND serverId = :statusId""" (SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit) """ ) - abstract suspend fun cleanup(accountId: Long, limit: Int) + abstract suspend fun cleanupStatuses(accountId: Long, limit: Int) /** * Cleans the TimelineAccountEntity table from accounts that are no longer referenced in the TimelineStatusEntity table @@ -167,6 +186,15 @@ AND timelineUserId = :accountId @Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1") abstract suspend fun getTopPlaceholderId(accountId: Long): String? + /** + * Returns the id directly above [serverId], or null if [serverId] is the id of the top status + */ + @Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(:serverId) < LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId < serverId)) ORDER BY LENGTH(serverId) ASC, serverId ASC LIMIT 1") + abstract suspend fun getIdAbove(accountId: Long, serverId: String): String? + + /** + * Returns the id of the next placeholder after [serverId] + */ @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? } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index ce816959..41b122c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -69,6 +69,7 @@ data class TimelineStatusEntity( val visibility: Status.Visibility, val attachments: String?, val mentions: String?, + val tags: String?, val application: String?, val reblogServerId: String?, // if it has a reblogged status, it's id is stored here val reblogAccountId: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 6a440d26..d85f6c45 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -22,23 +22,22 @@ import com.keylesspalace.tusky.EditProfileActivity import com.keylesspalace.tusky.FiltersActivity import com.keylesspalace.tusky.LicenseActivity import com.keylesspalace.tusky.ListsActivity -import com.keylesspalace.tusky.LoginActivity import com.keylesspalace.tusky.MainActivity -import com.keylesspalace.tusky.ModalTimelineActivity import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.ViewMediaActivity -import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewThreadActivity import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity +import com.keylesspalace.tusky.components.login.LoginActivity +import com.keylesspalace.tusky.components.login.LoginWebViewActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.report.ReportActivity -import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity +import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity import com.keylesspalace.tusky.components.search.SearchActivity import dagger.Module import dagger.android.ContributesAndroidInjector @@ -71,12 +70,6 @@ abstract class ActivitiesModule { @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesAccountListActivity(): AccountListActivity - @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) - abstract fun contributesModalTimelineActivity(): ModalTimelineActivity - - @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) - abstract fun contributesViewTagActivity(): ViewTagActivity - @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesViewThreadActivity(): ViewThreadActivity @@ -93,7 +86,7 @@ abstract class ActivitiesModule { abstract fun contributesLoginActivity(): LoginActivity @ContributesAndroidInjector - abstract fun contributesSplashActivity(): SplashActivity + abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesPreferencesActivity(): PreferencesActivity @@ -117,11 +110,14 @@ abstract class ActivitiesModule { abstract fun contributesInstanceListActivity(): InstanceListActivity @ContributesAndroidInjector - abstract fun contributesScheduledTootActivity(): ScheduledTootActivity + abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity @ContributesAndroidInjector abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity @ContributesAndroidInjector abstract fun contributesDraftActivity(): DraftsActivity + + @ContributesAndroidInjector + abstract fun contributesSplashActivity(): SplashActivity } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 7699ba69..b0f28261 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -61,7 +61,8 @@ class AppModule { AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), - AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28 + AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, + AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt index 156020f6..1d7510a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt @@ -15,12 +15,12 @@ package com.keylesspalace.tusky.di -import com.keylesspalace.tusky.service.SendTootService +import com.keylesspalace.tusky.service.SendStatusService import dagger.Module import dagger.android.ContributesAndroidInjector @Module abstract class ServicesModule { @ContributesAndroidInjector - abstract fun contributesSendTootService(): SendTootService + abstract fun contributesSendStatusService(): SendStatusService } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index 71d721f8..c8f746e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -10,7 +10,7 @@ import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.drafts.DraftsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel -import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel +import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel @@ -85,8 +85,8 @@ abstract class ViewModelModule { @Binds @IntoMap - @ViewModelKey(ScheduledTootViewModel::class) - internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel + @ViewModelKey(ScheduledStatusViewModel::class) + internal abstract fun scheduledStatusViewModel(viewModel: ScheduledStatusViewModel): ViewModel @Binds @IntoMap diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index 66c8022c..672bd5aa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -45,37 +45,57 @@ data class Account( localUsername } else displayName - override fun hashCode(): Int { - return id.hashCode() - } - - override fun equals(other: Any?): Boolean { - if (other !is Account) { - return false - } - return other.id == this.id - } - - fun deepEquals(other: Account): Boolean { - return id == other.id && - localUsername == other.localUsername && - displayName == other.displayName && - note == other.note && - url == other.url && - avatar == other.avatar && - header == other.header && - locked == other.locked && - followersCount == other.followersCount && - followingCount == other.followingCount && - statusesCount == other.statusesCount && - source == other.source && - bot == other.bot && - emojis == other.emojis && - fields == other.fields && - moved == other.moved - } - fun isRemote(): Boolean = this.username != this.localUsername + + /** + * overriding equals & hashcode because Spanned does not always compare correctly otherwise + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Account + + if (id != other.id) return false + if (localUsername != other.localUsername) return false + if (username != other.username) return false + if (displayName != other.displayName) return false + if (note.toString() != other.note.toString()) return false + if (url != other.url) return false + if (avatar != other.avatar) return false + if (header != other.header) return false + if (locked != other.locked) return false + if (followersCount != other.followersCount) return false + if (followingCount != other.followingCount) return false + if (statusesCount != other.statusesCount) return false + if (source != other.source) return false + if (bot != other.bot) return false + if (emojis != other.emojis) return false + if (fields != other.fields) return false + if (moved != other.moved) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + localUsername.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + (displayName?.hashCode() ?: 0) + result = 31 * result + note.toString().hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + avatar.hashCode() + result = 31 * result + header.hashCode() + result = 31 * result + locked.hashCode() + result = 31 * result + followersCount + result = 31 * result + followingCount + result = 31 * result + statusesCount + result = 31 * result + (source?.hashCode() ?: 0) + result = 31 * result + bot.hashCode() + result = 31 * result + (emojis?.hashCode() ?: 0) + result = 31 * result + (fields?.hashCode() ?: 0) + result = 31 * result + (moved?.hashCode() ?: 0) + return result + } } data class AccountSource( diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt index cb09981d..e5a547f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt @@ -19,7 +19,7 @@ import com.google.gson.annotations.SerializedName data class Conversation( val id: String, - val accounts: List, + val accounts: List, @SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038 val unread: Boolean ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt index a334257a..f3a4f65b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt @@ -1,3 +1,3 @@ package com.keylesspalace.tusky.entity -data class HashTag(val name: String) +data class HashTag(val name: String, val url: String) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index d1e2aca9..31fcca0e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -30,7 +30,8 @@ data class Instance( @SerializedName("contact_account") val contactAccount: Account, @SerializedName("max_toot_chars") val maxTootChars: Int?, @SerializedName("max_bio_chars") val maxBioChars: Int?, - @SerializedName("poll_limits") val pollLimits: PollLimits? + @SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, + val configuration: InstanceConfiguration?, ) { override fun hashCode(): Int { return uri.hashCode() @@ -45,7 +46,31 @@ data class Instance( } } -data class PollLimits( +data class PollConfiguration( @SerializedName("max_options") val maxOptions: Int?, - @SerializedName("max_option_chars") val maxOptionChars: Int? + @SerializedName("max_option_chars") val maxOptionChars: Int?, + @SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?, + @SerializedName("min_expiration") val minExpiration: Int?, + @SerializedName("max_expiration") val maxExpiration: Int?, +) + +data class InstanceConfiguration( + val statuses: StatusConfiguration?, + @SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?, + val polls: PollConfiguration?, +) + +data class StatusConfiguration( + @SerializedName("max_characters") val maxCharacters: Int?, + @SerializedName("max_media_attachments") val maxMediaAttachments: Int?, + @SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?, +) + +data class MediaAttachmentConfiguration( + @SerializedName("supported_mime_types") val supportedMimeTypes: List?, + @SerializedName("image_size_limit") val imageSizeLimit: Int?, + @SerializedName("image_matrix_limit") val imageMatrixLimit: Int?, + @SerializedName("video_size_limit") val videoSizeLimit: Int?, + @SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?, + @SerializedName("video_matrix_limit") val videoMatrixLimit: Int?, ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt new file mode 100644 index 00000000..15910f62 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt @@ -0,0 +1,9 @@ +package com.keylesspalace.tusky.entity + +/** + * The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/ + * We are only interested in the id, so other attributes are omitted + */ +data class MediaUploadResult( + val id: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index 6198867d..ae2d74a9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -24,7 +24,7 @@ import com.google.gson.annotations.JsonAdapter data class Notification( val type: Type, val id: String, - val account: Account, + val account: TimelineAccount, val status: Status? ) { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt index 18e3d71b..5bc78cf7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt @@ -16,7 +16,7 @@ package com.keylesspalace.tusky.entity data class SearchResult( - val accounts: List, + val accounts: List, val statuses: List, val hashtags: List ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 25588f1a..f75ce4e7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -25,7 +25,7 @@ import java.util.Date data class Status( val id: String, val url: String?, // not present if it's reblog - val account: Account, + val account: TimelineAccount, @SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, val reblog: Status?, @@ -42,6 +42,7 @@ data class Status( val visibility: Visibility, @SerializedName("media_attachments") var attachments: ArrayList, val mentions: List, + val tags: List?, val application: Application?, val pinned: Boolean?, val muted: Boolean?, @@ -148,6 +149,71 @@ data class Status( return builder.toString() } + /** + * overriding equals & hashcode because Spanned does not always compare correctly otherwise + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Status + + if (id != other.id) return false + if (url != other.url) return false + if (account != other.account) return false + if (inReplyToId != other.inReplyToId) return false + if (inReplyToAccountId != other.inReplyToAccountId) return false + if (reblog != other.reblog) return false + if (content.toString() != other.content.toString()) return false + if (createdAt != other.createdAt) return false + if (emojis != other.emojis) return false + if (reblogsCount != other.reblogsCount) return false + if (favouritesCount != other.favouritesCount) return false + if (reblogged != other.reblogged) return false + if (favourited != other.favourited) return false + if (bookmarked != other.bookmarked) return false + if (sensitive != other.sensitive) return false + if (spoilerText != other.spoilerText) return false + if (visibility != other.visibility) return false + if (attachments != other.attachments) return false + if (mentions != other.mentions) return false + if (tags != other.tags) return false + if (application != other.application) return false + if (pinned != other.pinned) return false + if (muted != other.muted) return false + if (poll != other.poll) return false + if (card != other.card) return false + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + (url?.hashCode() ?: 0) + result = 31 * result + account.hashCode() + result = 31 * result + (inReplyToId?.hashCode() ?: 0) + result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) + result = 31 * result + (reblog?.hashCode() ?: 0) + result = 31 * result + content.toString().hashCode() + result = 31 * result + createdAt.hashCode() + result = 31 * result + emojis.hashCode() + result = 31 * result + reblogsCount + result = 31 * result + favouritesCount + result = 31 * result + reblogged.hashCode() + result = 31 * result + favourited.hashCode() + result = 31 * result + bookmarked.hashCode() + result = 31 * result + sensitive.hashCode() + result = 31 * result + spoilerText.hashCode() + result = 31 * result + visibility.hashCode() + result = 31 * result + attachments.hashCode() + result = 31 * result + mentions.hashCode() + result = 31 * result + (tags?.hashCode() ?: 0) + result = 31 * result + (application?.hashCode() ?: 0) + result = 31 * result + (pinned?.hashCode() ?: 0) + result = 31 * result + (muted?.hashCode() ?: 0) + result = 31 * result + (poll?.hashCode() ?: 0) + result = 31 * result + (card?.hashCode() ?: 0) + return result + } + data class Mention( val id: String, val url: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt b/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt new file mode 100644 index 00000000..224129fe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt @@ -0,0 +1,39 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +/** + * Same as [Account], but only with the attributes required in timelines. + * Prefer this class over [Account] because it uses way less memory & deserializes faster from json. + */ +data class TimelineAccount( + val id: String, + @SerializedName("username") val localUsername: String, + @SerializedName("acct") val username: String, + @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract + val url: String, + val avatar: String, + val bot: Boolean = false, + val emojis: List? = emptyList(), // nullable for backward compatibility +) { + + val name: String + get() = if (displayName.isNullOrEmpty()) { + localUsername + } else displayName +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index fad5c58a..465b9f21 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -42,8 +42,8 @@ import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.databinding.FragmentAccountListBinding import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.settings.PrefKeys @@ -255,7 +255,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct followRequestsAdapter.removeItem(position) } - private fun getFetchCallByListType(fromId: String?): Single>> { + private fun getFetchCallByListType(fromId: String?): Single>> { return when (type) { Type.FOLLOWS -> { val accountId = requireId(type, id) @@ -313,7 +313,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct ) } - private fun onFetchAccountsSuccess(accounts: List, linkHeader: String?) { + private fun onFetchAccountsSuccess(accounts: List, linkHeader: String?) { adapter.setBottomLoading(false) val links = HttpHeaderLink.parse(linkHeader) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index c9395b38..b1a47ad8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -41,11 +41,10 @@ import androidx.lifecycle.Lifecycle; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BottomSheetActivity; -import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.PostLookupFallbackBehavior; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.StatusListActivity; import com.keylesspalace.tusky.ViewMediaActivity; -import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; import com.keylesspalace.tusky.components.report.ReportActivity; @@ -162,8 +161,6 @@ public abstract class SFragment extends Fragment implements Injectable { final String accountId = status.getActionableStatus().getAccount().getId(); final String accountUsername = status.getActionableStatus().getAccount().getUsername(); final String statusUrl = status.getActionableStatus().getUrl(); - List accounts = accountManager.getAllAccountsOrderedByActive(); - String openAsTitle = null; String loggedInAccountId = null; AccountEntity activeAccount = accountManager.getActiveAccount(); @@ -201,24 +198,12 @@ public abstract class SFragment extends Fragment implements Injectable { Menu menu = popup.getMenu(); MenuItem openAsItem = menu.findItem(R.id.status_open_as); - switch (accounts.size()) { - case 0: - case 1: - openAsItem.setVisible(false); - break; - case 2: - for (AccountEntity account : accounts) { - if (account != activeAccount) { - openAsTitle = String.format(getString(R.string.action_open_as), account.getFullName()); - break; - } - } - break; - default: - openAsTitle = String.format(getString(R.string.action_open_as), "…"); - break; + String openAsText = ((BaseActivity)getActivity()).getOpenAsText(); + if (openAsText == null) { + openAsItem.setVisible(false); + } else { + openAsItem.setTitle(openAsText); } - openAsItem.setTitle(openAsTitle); MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation); boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions()); @@ -231,7 +216,7 @@ public abstract class SFragment extends Fragment implements Injectable { popup.setOnMenuItemClickListener(item -> { switch (item.getItemId()) { - case R.id.status_share_content: { + case R.id.post_share_content: { Status statusToShare = status; if (statusToShare.getReblog() != null) statusToShare = statusToShare.getReblog(); @@ -245,15 +230,15 @@ public abstract class SFragment extends Fragment implements Injectable { sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to))); + startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_content_to))); return true; } - case R.id.status_share_link: { + case R.id.post_share_link: { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to))); + startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_link_to))); return true; } case R.id.status_copy_link: { @@ -378,15 +363,14 @@ public abstract class SFragment extends Fragment implements Injectable { } default: case UNKNOWN: { - LinkHelper.openLink(active.getAttachment().getUrl(), getContext()); + LinkHelper.openLink(requireContext(), active.getAttachment().getUrl()); break; } } } protected void viewTag(String tag) { - Intent intent = new Intent(getContext(), ViewTagActivity.class); - intent.putExtra("hashtag", tag); + Intent intent = StatusListActivity.newHashtagIntent(requireContext(), tag); startActivity(intent); } @@ -396,7 +380,7 @@ public abstract class SFragment extends Fragment implements Injectable { protected void showConfirmDeleteDialog(final String id, final int position) { new AlertDialog.Builder(getActivity()) - .setMessage(R.string.dialog_delete_toot_warning) + .setMessage(R.string.dialog_delete_post_warning) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { timelineCases.delete(id) .observeOn(AndroidSchedulers.mainThread()) @@ -419,7 +403,7 @@ public abstract class SFragment extends Fragment implements Injectable { return; } new AlertDialog.Builder(getActivity()) - .setMessage(R.string.dialog_redraft_toot_warning) + .setMessage(R.string.dialog_redraft_post_warning) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { timelineCases.delete(id) .observeOn(AndroidSchedulers.mainThread()) @@ -431,7 +415,7 @@ public abstract class SFragment extends Fragment implements Injectable { deletedStatus = status.toDeletedStatus(); } ComposeOptions composeOptions = new ComposeOptions(); - composeOptions.setTootText(deletedStatus.getText()); + composeOptions.setContent(deletedStatus.getText()); composeOptions.setInReplyToId(deletedStatus.getInReplyToId()); composeOptions.setVisibility(deletedStatus.getVisibility()); composeOptions.setContentWarning(deletedStatus.getSpoilerText()); @@ -456,18 +440,9 @@ public abstract class SFragment extends Fragment implements Injectable { .show(); } - private void openAsAccount(String statusUrl, AccountEntity account) { - accountManager.setActiveAccount(account); - Intent intent = new Intent(getContext(), MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(MainActivity.STATUS_URL, statusUrl); - startActivity(intent); - ((BaseActivity) getActivity()).finishWithoutSlideOutAnimation(); - } - private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) { BaseActivity activity = (BaseActivity) getActivity(); - activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account)); + activity.showAccountChooserDialog(dialogTitle, false, account -> activity.openAsAccount(statusUrl, account)); } private void downloadAllMedia(Status status) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 33828cab..1864ac1c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -330,7 +330,7 @@ public final class ViewThreadFragment extends SFragment implements // already viewing the status with this url // probably just a preview federated and the user is clicking again to view more -> open the browser // this can happen with some friendica statuses - LinkHelper.openLink(url, requireContext()); + LinkHelper.openLink(requireContext(), url); return; } super.onViewUrl(url); diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt similarity index 80% rename from app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java rename to app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt index 90599b22..56faefaf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt @@ -13,10 +13,10 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.interfaces; +package com.keylesspalace.tusky.interfaces -public interface LinkListener { - void onViewTag(String tag); - void onViewAccount(String id); - void onViewUrl(String url); +interface LinkListener { + fun onViewTag(tag: String) + fun onViewAccount(id: String) + fun onViewUrl(url: String) } diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt index ceb96f4a..60af6134 100644 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt @@ -19,6 +19,7 @@ import android.text.Spanned import android.text.SpannedString import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement @@ -32,12 +33,22 @@ import java.lang.reflect.Type class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { @Throws(JsonParseException::class) override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned { - /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which - * all status contents do, so it should be trimmed. */ - return json.asString?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString("") + return json.asString + /* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api. + * We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior. + */ + ?.replace("
", "
 ") + ?.replace("
", "
 ") + ?.replace("
", "
 ") + ?.replace(" ", "  ") + ?.parseAsHtml() + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * most status contents do, so it should be trimmed. */ + ?.trimTrailingWhitespace() + ?: SpannedString("") } override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(HtmlCompat.toHtml(src!!, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) + return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index a8b1050c..28d83eca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -28,6 +28,7 @@ import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.entity.MediaUploadResult import com.keylesspalace.tusky.entity.NewStatus import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Poll @@ -36,6 +37,7 @@ import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.StatusContext +import com.keylesspalace.tusky.entity.TimelineAccount import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import okhttp3.MultipartBody @@ -66,7 +68,7 @@ import retrofit2.http.Query interface MastodonApi { companion object { - const val ENDPOINT_AUTHORIZE = "/oauth/authorize" + const val ENDPOINT_AUTHORIZE = "oauth/authorize" const val DOMAIN_HEADER = "domain" const val PLACEHOLDER_DOMAIN = "dummy.placeholder" } @@ -142,11 +144,11 @@ interface MastodonApi { fun clearNotifications(): Single @Multipart - @POST("api/v1/media") + @POST("api/v2/media") fun uploadMedia( @Part file: MultipartBody.Part, @Part description: MultipartBody.Part? = null - ): Single + ): Single @FormUrlEncoded @PUT("api/v1/media/{mediaId}") @@ -177,13 +179,13 @@ interface MastodonApi { fun statusRebloggedBy( @Path("id") statusId: String, @Query("max_id") maxId: String? - ): Single>> + ): Single>> @GET("api/v1/statuses/{id}/favourited_by") fun statusFavouritedBy( @Path("id") statusId: String, @Query("max_id") maxId: String? - ): Single>> + ): Single>> @DELETE("api/v1/statuses/{id}") fun deleteStatus( @@ -285,7 +287,7 @@ interface MastodonApi { @Query("resolve") resolve: Boolean? = null, @Query("limit") limit: Int? = null, @Query("following") following: Boolean? = null - ): Single> + ): Single> @GET("api/v1/accounts/{id}") fun account( @@ -316,13 +318,13 @@ interface MastodonApi { fun accountFollowers( @Path("id") accountId: String, @Query("max_id") maxId: String? - ): Single>> + ): Single>> @GET("api/v1/accounts/{id}/following") fun accountFollowing( @Path("id") accountId: String, @Query("max_id") maxId: String? - ): Single>> + ): Single>> @FormUrlEncoded @POST("api/v1/accounts/{id}/follow") @@ -383,12 +385,12 @@ interface MastodonApi { @GET("api/v1/blocks") fun blocks( @Query("max_id") maxId: String? - ): Single>> + ): Single>> @GET("api/v1/mutes") fun mutes( @Query("max_id") maxId: String? - ): Single>> + ): Single>> @GET("api/v1/domain_blocks") fun domainBlocks( @@ -425,7 +427,7 @@ interface MastodonApi { @GET("api/v1/follow_requests") fun followRequests( @Query("max_id") maxId: String? - ): Single>> + ): Single>> @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( @@ -439,24 +441,24 @@ interface MastodonApi { @FormUrlEncoded @POST("api/v1/apps") - fun authenticateApp( + suspend fun authenticateApp( @Header(DOMAIN_HEADER) domain: String, @Field("client_name") clientName: String, @Field("redirect_uris") redirectUris: String, @Field("scopes") scopes: String, @Field("website") website: String - ): Call + ): AppCredentials @FormUrlEncoded @POST("oauth/token") - fun fetchOAuthToken( + suspend fun fetchOAuthToken( @Header(DOMAIN_HEADER) domain: String, @Field("client_id") clientId: String, @Field("client_secret") clientSecret: String, @Field("redirect_uri") redirectUri: String, @Field("code") code: String, @Field("grant_type") grantType: String - ): Call + ): AccessToken @FormUrlEncoded @POST("api/v1/lists") @@ -480,7 +482,7 @@ interface MastodonApi { fun getAccountsInList( @Path("listId") listId: String, @Query("limit") limit: Int - ): Single> + ): Single> @FormUrlEncoded // @DELETE doesn't support fields diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 25dd91d4..6101dd84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -18,19 +18,19 @@ package com.keylesspalace.tusky.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.graphics.Color import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat +import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.compose.ComposeActivity -import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.service.SendTootService -import com.keylesspalace.tusky.service.TootToSend +import com.keylesspalace.tusky.service.SendStatusService +import com.keylesspalace.tusky.service.StatusToSend import com.keylesspalace.tusky.util.randomAlphanumericString import dagger.android.AndroidInjection import javax.inject.Inject @@ -45,22 +45,19 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { AndroidInjection.inject(this, context) - val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1) - val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1) - val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER) - val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME) - val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) - val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility - val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: "" - val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray() - val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT) - val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL) - - val account = accountManager.getAccountById(senderId) - - val notificationManager = NotificationManagerCompat.from(context) - if (intent.action == NotificationHelper.REPLY_ACTION) { + val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1) + val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1) + val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER) + val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME) + val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) + val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility + val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: "" + val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray() + + val account = accountManager.getAccountById(senderId) + + val notificationManager = NotificationManagerCompat.from(context) val message = getReplyMessage(intent) @@ -85,9 +82,9 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { } else { val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString() - val sendIntent = SendTootService.sendTootIntent( + val sendIntent = SendStatusService.sendStatusIntent( context, - TootToSend( + StatusToSend( text = text, warningText = spoiler, visibility = visibility.serverString(), @@ -109,14 +106,20 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { context.startService(sendIntent) + val color = if (BuildConfig.FLAVOR == "green") { + Color.parseColor("#19A341") + } else { + ContextCompat.getColor(context, R.color.tusky_blue) + } + val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, (R.color.chinwag_green))) + .setColor(color) .setGroup(senderFullName) .setDefaults(0) // So it doesn't ring twice, notify only in Target callback - builder.setContentTitle(context.getString(R.string.status_sent)) - builder.setContentText(context.getString(R.string.status_sent_long)) + builder.setContentTitle(context.getString(R.string.post_sent)) + builder.setContentText(context.getString(R.string.post_sent_long)) builder.setSubText(senderFullName) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) @@ -125,29 +128,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { notificationManager.notify(notificationId, builder.build()) } - } else if (intent.action == NotificationHelper.COMPOSE_ACTION) { - - context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) - - notificationManager.cancel(notificationId) - - accountManager.setActiveAccount(senderId) - - val composeIntent = ComposeActivity.startIntent( - context, - ComposeOptions( - inReplyToId = citedStatusId, - replyVisibility = visibility, - contentWarning = spoiler, - mentionedUsernames = mentions.toSet(), - replyingStatusAuthor = localAuthorId, - replyingStatusContent = citedText - ) - ) - - composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - context.startActivity(composeIntent) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt similarity index 50% rename from app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt rename to app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index d2092118..a2709f97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -19,8 +19,8 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusComposedEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewStatus @@ -30,18 +30,17 @@ import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import java.util.Timer -import java.util.TimerTask import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import javax.inject.Inject -class SendTootService : Service(), Injectable { +class SendStatusService : Service(), Injectable { @Inject lateinit var mastodonApi: MastodonApi @@ -50,18 +49,14 @@ class SendTootService : Service(), Injectable { @Inject lateinit var eventHub: EventHub @Inject - lateinit var database: AppDatabase - @Inject lateinit var draftHelper: DraftHelper private val supervisorJob = SupervisorJob() private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob) - private val tootsToSend = ConcurrentHashMap() + private val statusesToSend = ConcurrentHashMap() private val sendCalls = ConcurrentHashMap>() - private val timer = Timer() - private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } override fun onCreate() { @@ -75,38 +70,38 @@ class SendTootService : Service(), Injectable { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - if (intent.hasExtra(KEY_TOOT)) { - val tootToSend = intent.getParcelableExtra(KEY_TOOT) - ?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra") + if (intent.hasExtra(KEY_STATUS)) { + val statusToSend = intent.getParcelableExtra(KEY_STATUS) + ?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW) + val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_post_notification_channel_name), NotificationManager.IMPORTANCE_LOW) notificationManager.createNotificationChannel(channel) } - var notificationText = tootToSend.warningText + var notificationText = statusToSend.warningText if (notificationText.isBlank()) { - notificationText = tootToSend.text + notificationText = statusToSend.text } val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_title)) + .setContentTitle(getString(R.string.send_post_notification_title)) .setContentText(notificationText) .setProgress(1, 0, true) .setOngoing(true) - .setColor(ContextCompat.getColor(this, R.color.chinwag_green)) + .setColor(ContextCompat.getColor(this, R.color.notification_color)) .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId)) - if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (statusesToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) startForeground(sendingNotificationId, builder.build()) } else { notificationManager.notify(sendingNotificationId, builder.build()) } - tootsToSend[sendingNotificationId] = tootToSend - sendToot(sendingNotificationId--) + statusesToSend[sendingNotificationId] = statusToSend + sendStatus(sendingNotificationId--) } else { if (intent.hasExtra(KEY_CANCEL)) { @@ -117,95 +112,95 @@ class SendTootService : Service(), Injectable { return START_NOT_STICKY } - private fun sendToot(tootId: Int) { + private fun sendStatus(statusId: Int) { - // when tootToSend == null, sending has been canceled - val tootToSend = tootsToSend[tootId] ?: return + // when statusToSend == null, sending has been canceled + val statusToSend = statusesToSend[statusId] ?: return // when account == null, user has logged out, cancel sending - val account = accountManager.getAccountById(tootToSend.accountId) + val account = accountManager.getAccountById(statusToSend.accountId) if (account == null) { - tootsToSend.remove(tootId) - notificationManager.cancel(tootId) + statusesToSend.remove(statusId) + notificationManager.cancel(statusId) stopSelfWhenDone() return } - tootToSend.retries++ + statusToSend.retries++ val newStatus = NewStatus( - tootToSend.text, - tootToSend.warningText, - tootToSend.inReplyToId, - tootToSend.visibility, - tootToSend.sensitive, - tootToSend.mediaIds, - tootToSend.scheduledAt, - tootToSend.poll + statusToSend.text, + statusToSend.warningText, + statusToSend.inReplyToId, + statusToSend.visibility, + statusToSend.sensitive, + statusToSend.mediaIds, + statusToSend.scheduledAt, + statusToSend.poll ) val sendCall = mastodonApi.createStatus( "Bearer " + account.accessToken, account.domain, - tootToSend.idempotencyKey, + statusToSend.idempotencyKey, newStatus ) - sendCalls[tootId] = sendCall + sendCalls[statusId] = sendCall val callback = object : Callback { override fun onResponse(call: Call, response: Response) { + serviceScope.launch { - val scheduled = !tootToSend.scheduledAt.isNullOrEmpty() - tootsToSend.remove(tootId) + val scheduled = !statusToSend.scheduledAt.isNullOrEmpty() + statusesToSend.remove(statusId) - if (response.isSuccessful) { - // If the status was loaded from a draft, delete the draft and associated media files. - if (tootToSend.draftId != 0) { - serviceScope.launch { - draftHelper.deleteDraftAndAttachments(tootToSend.draftId) + if (response.isSuccessful) { + // If the status was loaded from a draft, delete the draft and associated media files. + if (statusToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(statusToSend.draftId) } - } - if (scheduled) { - response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) + if (scheduled) { + response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) + } else { + response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch) + } + + notificationManager.cancel(statusId) } else { - response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch) + // the server refused to accept the status, save status & show error message + saveStatusToDrafts(statusToSend) + + val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_post_notification_error_title)) + .setContentText(getString(R.string.send_post_notification_saved_content)) + .setColor( + ContextCompat.getColor( + this@SendStatusService, + R.color.notification_color + ) + ) + + notificationManager.cancel(statusId) + notificationManager.notify(errorNotificationId--, builder.build()) } - - notificationManager.cancel(tootId) - } else { - // the server refused to accept the toot, save toot & show error message - saveTootToDrafts(tootToSend) - - val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_error_title)) - .setContentText(getString(R.string.send_toot_notification_saved_content)) - .setColor(ContextCompat.getColor(this@SendTootService, R.color.chinwag_green)) - - notificationManager.cancel(tootId) - notificationManager.notify(errorNotificationId--, builder.build()) + stopSelfWhenDone() } - - stopSelfWhenDone() } override fun onFailure(call: Call, t: Throwable) { - var backoff = TimeUnit.SECONDS.toMillis(tootToSend.retries.toLong()) - if (backoff > MAX_RETRY_INTERVAL) { - backoff = MAX_RETRY_INTERVAL - } + serviceScope.launch { + var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()) + if (backoff > MAX_RETRY_INTERVAL) { + backoff = MAX_RETRY_INTERVAL + } - timer.schedule( - object : TimerTask() { - override fun run() { - sendToot(tootId) - } - }, - backoff - ) + delay(backoff) + sendStatus(statusId) + } } } @@ -214,65 +209,52 @@ class SendTootService : Service(), Injectable { private fun stopSelfWhenDone() { - if (tootsToSend.isEmpty()) { - ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE) + if (statusesToSend.isEmpty()) { + ServiceCompat.stopForeground(this@SendStatusService, ServiceCompat.STOP_FOREGROUND_REMOVE) stopSelf() } } - private fun cancelSending(tootId: Int) { - val tootToCancel = tootsToSend.remove(tootId) - if (tootToCancel != null) { - val sendCall = sendCalls.remove(tootId) + private fun cancelSending(statusId: Int) = serviceScope.launch { + val statusToCancel = statusesToSend.remove(statusId) + if (statusToCancel != null) { + val sendCall = sendCalls.remove(statusId) sendCall?.cancel() - saveTootToDrafts(tootToCancel) + saveStatusToDrafts(statusToCancel) - val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) + val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_cancel_title)) - .setContentText(getString(R.string.send_toot_notification_saved_content)) - .setColor(ContextCompat.getColor(this@SendTootService, R.color.chinwag_green)) + .setContentTitle(getString(R.string.send_post_notification_cancel_title)) + .setContentText(getString(R.string.send_post_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendStatusService, R.color.notification_color)) - notificationManager.notify(tootId, builder.build()) + notificationManager.notify(statusId, builder.build()) - timer.schedule( - object : TimerTask() { - override fun run() { - notificationManager.cancel(tootId) - stopSelfWhenDone() - } - }, - 5000 - ) + delay(5000) } } - private fun saveTootToDrafts(toot: TootToSend) { - serviceScope.launch { - draftHelper.saveDraft( - draftId = toot.draftId, - accountId = toot.accountId, - inReplyToId = toot.inReplyToId, - content = toot.text, - contentWarning = toot.warningText, - sensitive = toot.sensitive, - visibility = Status.Visibility.byString(toot.visibility), - mediaUris = toot.mediaUris, - mediaDescriptions = toot.mediaDescriptions, - poll = toot.poll, - failedToSend = true - ) - } + private suspend fun saveStatusToDrafts(status: StatusToSend) { + draftHelper.saveDraft( + draftId = status.draftId, + accountId = status.accountId, + inReplyToId = status.inReplyToId, + content = status.text, + contentWarning = status.warningText, + sensitive = status.sensitive, + visibility = Status.Visibility.byString(status.visibility), + mediaUris = status.mediaUris, + mediaDescriptions = status.mediaDescriptions, + poll = status.poll, + failedToSend = true + ) } - private fun cancelSendingIntent(tootId: Int): PendingIntent { - - val intent = Intent(this, SendTootService::class.java) - - intent.putExtra(KEY_CANCEL, tootId) - - return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT) + private fun cancelSendingIntent(statusId: Int): PendingIntent { + val intent = Intent(this, SendStatusService::class.java) + intent.putExtra(KEY_CANCEL, statusId) + return PendingIntent.getService(this, statusId, intent, NotificationHelper.pendingIntentFlags(false)) } override fun onDestroy() { @@ -282,7 +264,7 @@ class SendTootService : Service(), Injectable { companion object { - private const val KEY_TOOT = "toot" + private const val KEY_STATUS = "status" private const val KEY_CANCEL = "cancel_id" private const val CHANNEL_ID = "send_toots" @@ -292,21 +274,21 @@ class SendTootService : Service(), Injectable { private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis @JvmStatic - fun sendTootIntent( + fun sendStatusIntent( context: Context, - tootToSend: TootToSend + statusToSend: StatusToSend ): Intent { - val intent = Intent(context, SendTootService::class.java) - intent.putExtra(KEY_TOOT, tootToSend) + val intent = Intent(context, SendStatusService::class.java) + intent.putExtra(KEY_STATUS, statusToSend) - if (tootToSend.mediaUris.isNotEmpty()) { + if (statusToSend.mediaUris.isNotEmpty()) { // forward uri permissions intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val uriClip = ClipData( - ClipDescription("Toot Media", arrayOf("image/*", "video/*")), - ClipData.Item(tootToSend.mediaUris[0]) + ClipDescription("Status Media", arrayOf("image/*", "video/*")), + ClipData.Item(statusToSend.mediaUris[0]) ) - tootToSend.mediaUris + statusToSend.mediaUris .drop(1) .forEach { mediaUri -> uriClip.addItem(ClipData.Item(mediaUri)) @@ -321,7 +303,7 @@ class SendTootService : Service(), Injectable { } @Parcelize -data class TootToSend( +data class StatusToSend( val text: String, val warningText: String, val visibility: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt index 9ac5adac..d9d08ff7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt @@ -20,8 +20,8 @@ import androidx.core.content.ContextCompat import javax.inject.Inject class ServiceClient @Inject constructor(private val context: Context) { - fun sendToot(tootToSend: TootToSend) { - val intent = SendTootService.sendTootIntent(context, tootToSend) + fun sendToot(tootToSend: StatusToSend) { + val intent = SendStatusService.sendStatusIntent(context, tootToSend) ContextCompat.startForegroundService(context, intent) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt index f513feee..385be6c1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -280,7 +280,7 @@ class EmojiCompatFont( R.string.caption_blobmoji, R.drawable.ic_blobmoji, "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", - "12.0.0" + "14.0.1" ) val TWEMOJI = EmojiCompatFont( "Twemoji", @@ -288,7 +288,7 @@ class EmojiCompatFont( R.string.caption_twemoji, R.drawable.ic_twemoji, "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", - "12.0.0" + "14.0.0" ) val NOTOEMOJI = EmojiCompatFont( "NotoEmoji", @@ -296,7 +296,7 @@ class EmojiCompatFont( R.string.caption_notoemoji, R.drawable.ic_notoemoji, "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", - "11.0.0" + "14.0.0" ) /** diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java deleted file mode 100644 index 4969c9ad..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ /dev/null @@ -1,251 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.util; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.URLSpan; -import android.util.Log; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.browser.customtabs.CustomTabColorSchemeParams; -import androidx.browser.customtabs.CustomTabsIntent; -import androidx.preference.PreferenceManager; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.interfaces.LinkListener; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - -public class LinkHelper { - public static String getDomain(String urlString) { - URI uri; - try { - uri = new URI(urlString); - } catch (URISyntaxException e) { - return ""; - } - String host = uri.getHost(); - if(host == null) { - return ""; - } else if (host.startsWith("www.")) { - return host.substring(4); - } else { - return host; - } - } - - /** - * Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating - * them with callbacks to notify when they're clicked. - * - * @param view the returned text will be put in - * @param content containing text with mentions, links, or hashtags - * @param mentions any '@' mentions which are known to be in the content - * @param listener to notify about particular spans that are clicked - */ - public static void setClickableText(TextView view, CharSequence content, - @Nullable List mentions, final LinkListener listener) { - SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content); - URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class); - for (URLSpan span : urlSpans) { - int start = builder.getSpanStart(span); - int end = builder.getSpanEnd(span); - int flags = builder.getSpanFlags(span); - CharSequence text = builder.subSequence(start, end); - ClickableSpan customSpan = null; - - if (text.charAt(0) == '#') { - final String tag = text.subSequence(1, text.length()).toString(); - customSpan = new NoUnderlineURLSpan(span.getURL()) { - @Override - public void onClick(@NonNull View widget) { listener.onViewTag(tag); } - }; - } else if (text.charAt(0) == '@' && mentions != null && mentions.size() > 0) { - // https://github.com/tuskyapp/Tusky/pull/2339 - String id = null; - for (Status.Mention mention : mentions) { - if (mention.getUrl().equals(span.getURL())) { - id = mention.getId(); - break; - } - } - if (id != null) { - final String accountId = id; - customSpan = new NoUnderlineURLSpan(span.getURL()) { - @Override - public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } - }; - } - } - - if (customSpan == null) { - customSpan = new NoUnderlineURLSpan(span.getURL()) { - @Override - public void onClick(@NonNull View widget) { - listener.onViewUrl(getURL()); - } - }; - } - builder.removeSpan(span); - builder.setSpan(customSpan, start, end, flags); - - /* Add zero-width space after links in end of line to fix its too large hitbox. - * See also : https://github.com/tuskyapp/Tusky/issues/846 - * https://github.com/tuskyapp/Tusky/pull/916 */ - if (end >= builder.length() || - builder.subSequence(end, end + 1).toString().equals("\n")){ - builder.insert(end, "\u200B"); - } - } - - view.setText(builder); - view.setMovementMethod(LinkMovementMethod.getInstance()); - } - - /** - * Put mentions in a piece of text and makes them clickable, associating them with callbacks to - * notify when they're clicked. - * - * @param view the returned text will be put in - * @param mentions any '@' mentions which are known to be in the content - * @param listener to notify about particular spans that are clicked - */ - public static void setClickableMentions( - TextView view, @Nullable List mentions, final LinkListener listener) { - if (mentions == null || mentions.size() == 0) { - view.setText(null); - return; - } - SpannableStringBuilder builder = new SpannableStringBuilder(); - int start = 0; - int end = 0; - int flags; - boolean firstMention = true; - for (Status.Mention mention : mentions) { - String accountUsername = mention.getLocalUsername(); - final String accountId = mention.getId(); - ClickableSpan customSpan = new NoUnderlineURLSpan(mention.getUrl()) { - @Override - public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } - }; - - end += 1 + accountUsername.length(); // length of @ + username - flags = builder.getSpanFlags(customSpan); - if (firstMention) { - firstMention = false; - } else { - builder.append(" "); - start += 1; - end += 1; - } - builder.append("@"); - builder.append(accountUsername); - builder.setSpan(customSpan, start, end, flags); - builder.append("\u200B"); // same reasonning than in setClickableText - end += 1; // shift position to take the previous character into account - start = end; - } - view.setText(builder); - view.setMovementMethod(LinkMovementMethod.getInstance()); - } - - public static CharSequence createClickableText(String text, String link) { - URLSpan span = new NoUnderlineURLSpan(link); - - SpannableStringBuilder clickableText = new SpannableStringBuilder(text); - clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - return clickableText; - } - - /** - * Opens a link, depending on the settings, either in the browser or in a custom tab - * - * @param url a string containing the url to open - * @param context context - */ - public static void openLink(String url, Context context) { - Uri uri = Uri.parse(url).normalizeScheme(); - - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("customTabs", false); - if (useCustomTabs) { - openLinkInCustomTab(uri, context); - } else { - openLinkInBrowser(uri, context); - } - } - - /** - * opens a link in the browser via Intent.ACTION_VIEW - * - * @param uri the uri to open - * @param context context - */ - public static void openLinkInBrowser(Uri uri, Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - Log.w("LinkHelper", "Actvity was not found for intent, " + intent); - } - } - - /** - * tries to open a link in a custom tab - * falls back to browser if not possible - * - * @param uri the uri to open - * @param context context - */ - public static void openLinkInCustomTab(Uri uri, Context context) { - int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface); - int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor); - int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor); - - CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() - .setToolbarColor(toolbarColor) - .setNavigationBarColor(navigationbarColor) - .setNavigationBarDividerColor(navigationbarDividerColor) - .build(); - - CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build(); - - try { - customTabsIntent.launchUrl(context, uri); - } catch (ActivityNotFoundException e) { - Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent); - openLinkInBrowser(uri, context); - } - - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt new file mode 100644 index 00000000..1abbab14 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -0,0 +1,240 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +@file:JvmName("LinkHelper") + +package com.keylesspalace.tusky.util + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.URLSpan +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Status.Mention +import com.keylesspalace.tusky.interfaces.LinkListener + +fun getDomain(urlString: String?): String { + val host = urlString?.toUri()?.host + return when { + host == null -> "" + host.startsWith("www.") -> host.substring(4) + else -> host + } +} + +/** + * Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating + * them with callbacks to notify when they're clicked. + * + * @param view the returned text will be put in + * @param content containing text with mentions, links, or hashtags + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ +fun setClickableText(view: TextView, content: CharSequence, mentions: List, tags: List?, listener: LinkListener) { + view.text = SpannableStringBuilder.valueOf(content).apply { + getSpans(0, content.length, URLSpan::class.java).forEach { + setClickableText(it, this, mentions, tags, listener) + } + } + view.movementMethod = LinkMovementMethod.getInstance() +} + +@VisibleForTesting +fun setClickableText( + span: URLSpan, + builder: SpannableStringBuilder, + mentions: List, + tags: List?, + listener: LinkListener +) = builder.apply { + val start = getSpanStart(span) + val end = getSpanEnd(span) + val flags = getSpanFlags(span) + val text = subSequence(start, end) + + val customSpan = when (text[0]) { + '#' -> getCustomSpanForTag(text, tags, span, listener) + '@' -> getCustomSpanForMention(mentions, span, listener) + else -> null + } ?: object : NoUnderlineURLSpan(span.url) { + override fun onClick(view: View) = listener.onViewUrl(url) + } + + removeSpan(span) + setSpan(customSpan, start, end, flags) + + /* Add zero-width space after links in end of line to fix its too large hitbox. + * See also : https://github.com/tuskyapp/Tusky/issues/846 + * https://github.com/tuskyapp/Tusky/pull/916 */ + if (end >= length || subSequence(end, end + 1).toString() == "\n") { + insert(end, "\u200B") + } +} + +@VisibleForTesting +fun getTagName(text: CharSequence, tags: List?): String? { + val scrapedName = text.subSequence(1, text.length).toString() + return when (tags) { + null -> scrapedName + else -> tags.firstOrNull { it.name.equals(scrapedName, true) }?.name + } +} + +private fun getCustomSpanForTag(text: CharSequence, tags: List?, span: URLSpan, listener: LinkListener): ClickableSpan? { + return getTagName(text, tags)?.let { + object : NoUnderlineURLSpan(span.url) { + override fun onClick(view: View) = listener.onViewTag(it) + } + } +} + +private fun getCustomSpanForMention(mentions: List, span: URLSpan, listener: LinkListener): ClickableSpan? { + // https://github.com/tuskyapp/Tusky/pull/2339 + return mentions.firstOrNull { it.url == span.url }?.let { + getCustomSpanForMentionUrl(span.url, it.id, listener) + } +} + +private fun getCustomSpanForMentionUrl(url: String, mentionId: String, listener: LinkListener): ClickableSpan { + return object : NoUnderlineURLSpan(url) { + override fun onClick(view: View) = listener.onViewAccount(mentionId) + } +} + +/** + * Put mentions in a piece of text and makes them clickable, associating them with callbacks to + * notify when they're clicked. + * + * @param view the returned text will be put in + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ +fun setClickableMentions(view: TextView, mentions: List?, listener: LinkListener) { + if (mentions?.isEmpty() != false) { + view.text = null + return + } + + view.text = SpannableStringBuilder().apply { + var start = 0 + var end = 0 + var flags: Int + var firstMention = true + + for (mention in mentions) { + val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener) + end += 1 + mention.localUsername.length // length of @ + username + flags = getSpanFlags(customSpan) + if (firstMention) { + firstMention = false + } else { + append(" ") + start += 1 + end += 1 + } + + append("@") + append(mention.localUsername) + setSpan(customSpan, start, end, flags) + append("\u200B") // same reasoning as in setClickableText + end += 1 // shift position to take the previous character into account + start = end + } + } + view.movementMethod = LinkMovementMethod.getInstance() +} + +fun createClickableText(text: String, link: String): CharSequence { + return SpannableStringBuilder(text).apply { + setSpan(NoUnderlineURLSpan(link), 0, text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } +} + +/** + * Opens a link, depending on the settings, either in the browser or in a custom tab + * + * @receiver the Context to open the link from + * @param url a string containing the url to open + */ +fun Context.openLink(url: String) { + val uri = url.toUri().normalizeScheme() + val useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("customTabs", false) + + if (useCustomTabs) { + openLinkInCustomTab(uri, this) + } else { + openLinkInBrowser(uri, this) + } +} + +/** + * opens a link in the browser via Intent.ACTION_VIEW + * + * @param uri the uri to open + * @param context context + */ +private fun openLinkInBrowser(uri: Uri?, context: Context) { + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "Actvity was not found for intent, $intent") + } +} + +/** + * tries to open a link in a custom tab + * falls back to browser if not possible + * + * @param uri the uri to open + * @param context context + */ +private fun openLinkInCustomTab(uri: Uri, context: Context) { + val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) + val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) + val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) + val colorSchemeParams = CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build() + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build() + + try { + customTabsIntent.launchUrl(context, uri) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "Activity was not found for intent $customTabsIntent") + openLinkInBrowser(uri, context) + } +} + +private const val TAG = "LinkHelper" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 879fccc3..501f9056 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -182,7 +182,7 @@ class ListStatusAccessibilityDelegate( android.R.layout.simple_list_item_1, textLinks ) - ) { _, which -> LinkHelper.openLink(links[which].link, host.context) } + ) { _, which -> host.context.openLink(links[which].link) } .show() .let { forceFocus(it.listView) } } @@ -270,12 +270,12 @@ class ListStatusAccessibilityDelegate( private val collapseCwAction = AccessibilityActionCompat( R.id.action_collapse_cw, - context.getString(R.string.status_content_warning_show_less) + context.getString(R.string.post_content_warning_show_less) ) private val expandCwAction = AccessibilityActionCompat( R.id.action_expand_cw, - context.getString(R.string.status_content_warning_show_more) + context.getString(R.string.post_content_warning_show_more) ) private val replyAction = AccessibilityActionCompat( diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt index a9b56b89..779a7e6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt @@ -29,6 +29,6 @@ open class NoUnderlineURLSpan( } override fun onClick(view: View) { - LinkHelper.openLink(url, view.context) + view.context.openLink(url) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index f2e48f4a..60ac73f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -176,9 +176,9 @@ class StatusViewHelper(private val itemView: View) { sensitiveMediaShow.visibility = View.GONE } else { sensitiveMediaWarning.text = if (sensitive) { - context.getString(R.string.status_sensitive_media_title) + context.getString(R.string.post_sensitive_media_title) } else { - context.getString(R.string.status_media_hidden_title) + context.getString(R.string.post_media_hidden_title) } sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE @@ -225,7 +225,7 @@ class StatusViewHelper(private val itemView: View) { val context = mediaLabel.context var labelText = getLabelTypeText(context, attachments[0].type) if (sensitive) { - val sensitiveText = context.getString(R.string.status_sensitive_media_title) + val sensitiveText = context.getString(R.string.post_sensitive_media_title) labelText += String.format(" (%s)", sensitiveText) } mediaLabel.text = labelText @@ -239,10 +239,10 @@ class StatusViewHelper(private val itemView: View) { private fun getLabelTypeText(context: Context, type: Attachment.Type): String { return when (type) { - Attachment.Type.IMAGE -> context.getString(R.string.status_media_images) - Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video) - Attachment.Type.AUDIO -> context.getString(R.string.status_media_audio) - else -> context.getString(R.string.status_media_attachments) + Attachment.Type.IMAGE -> context.getString(R.string.post_media_images) + Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.post_media_video) + Attachment.Type.AUDIO -> context.getString(R.string.post_media_audio) + else -> context.getString(R.string.post_media_attachments) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt index 57b87f92..7a4a3659 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -16,44 +16,6 @@ fun randomAlphanumericString(count: Int): String { return String(chars) } -// We sort statuses by ID. Something we need to invent some ID for placeholder. -// Not sure if inc()/dec() should be made `operator` or not - -/** - * "Increment" string so that during sorting it's bigger than [this]. - */ -fun String.inc(): String { - // We assume that we will stay in the safe range for now - val builder = this.toCharArray() - builder[lastIndex] = builder[lastIndex].inc() - return String(builder) -} - -/** - * "Decrement" string so that during sorting it's smaller than [this]. - */ -fun String.dec(): String { - if (this.isEmpty()) return this - - val builder = this.toCharArray() - var i = builder.lastIndex - while (i > 0) { - if (builder[i] > '0') { - builder[i] = builder[i].dec() - return String(builder) - } else { - builder[i] = 'z' - } - i-- - } - return if (builder[0] > '1') { - builder[0] = builder[0].dec() - String(builder) - } else { - String(builder.copyOfRange(1, builder.size)) - } -} - /** * A < B (strictly) by length and then by content. * Examples: @@ -71,13 +33,17 @@ fun String.isLessThan(other: String): Boolean { } } -fun String.idCompareTo(other: String): Int { - return when { - this === other -> 0 - this.length < other.length -> -1 - this.length > other.length -> 1 - else -> this.compareTo(other) - } +/** + * A <= B (strictly) by length and then by content. + * Examples: + * "abc" <= "bcd" + * "ab" <= "abc" + * "cb" <= "abc" + * "ab" <= "ab" + * not: "abc" > "cb" + */ +fun String.isLessThanOrEqual(other: String): Boolean { + return this == other || isLessThan(other) } fun Spanned.trimTrailingWhitespace(): Spanned { diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt index 116f0170..59b35924 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -21,9 +21,9 @@ import android.view.LayoutInflater import com.google.android.material.card.MaterialCardView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.CardLicenseBinding -import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink class LicenseCard @JvmOverloads constructor( @@ -50,7 +50,7 @@ class LicenseCard binding.licenseCardLink.hide() } else { binding.licenseCardLink.text = link - setOnClickListener { LinkHelper.openLink(link, context) } + setOnClickListener { context.openLink(link) } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 409b858d..75f90ca4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.TimelineAccount; import java.util.Objects; @@ -44,11 +45,11 @@ public abstract class NotificationViewData { public static final class Concrete extends NotificationViewData { private final Notification.Type type; private final String id; - private final Account account; + private final TimelineAccount account; @Nullable private final StatusViewData.Concrete statusViewData; - public Concrete(Notification.Type type, String id, Account account, + public Concrete(Notification.Type type, String id, TimelineAccount account, @Nullable StatusViewData.Concrete statusViewData) { this.type = type; this.id = id; @@ -64,7 +65,7 @@ public abstract class NotificationViewData { return id; } - public Account getAccount() { + public TimelineAccount getAccount() { return account; } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index 92675eb6..d8f27157 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -22,12 +22,11 @@ import com.keylesspalace.tusky.entity.Status /** * Created by charlag on 11/07/2017. * - * * Class to represent data required to display either a notification or a placeholder. * It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder]. */ -sealed class StatusViewData private constructor() { - abstract val viewDataId: Long +sealed class StatusViewData { + abstract val id: String data class Concrete( val status: Status, @@ -49,8 +48,8 @@ sealed class StatusViewData private constructor() { /** Whether the status meets the requirement to be collapse */ val isCollapsed: Boolean, ) : StatusViewData() { - override val viewDataId: Long - get() = status.id.hashCode().toLong() + override val id: String + get() = status.id val content: Spanned val spoilerText: String @@ -116,9 +115,6 @@ sealed class StatusViewData private constructor() { } } - val id: String - get() = status.id - /** Helper for Java */ fun copyWithStatus(status: Status): Concrete { return copy(status = status) @@ -140,10 +136,10 @@ sealed class StatusViewData private constructor() { } } - data class Placeholder(val id: String, val isLoading: Boolean) : StatusViewData() { - override val viewDataId: Long - get() = id.hashCode().toLong() - } + data class Placeholder( + override val id: String, + val isLoading: Boolean + ) : StatusViewData() fun asStatusOrNull() = this as? Concrete diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt index b02c2ac0..fd989376 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log -import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Either.Left @@ -28,7 +28,7 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.subjects.BehaviorSubject import javax.inject.Inject -data class State(val accounts: Either>, val searchResult: List?) +data class State(val accounts: Either>, val searchResult: List?) class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() { @@ -49,7 +49,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) } } - fun addAccountToList(listId: String, account: Account) { + fun addAccountToList(listId: String, account: TimelineAccount) { api.addCountToList(listId, listOf(account.id)) .subscribe( { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index a51fea0d..f3539f8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -15,15 +15,11 @@ package com.keylesspalace.tusky.viewmodel -import android.content.Context -import android.graphics.Bitmap +import android.app.Application import android.net.Uri -import android.util.Log +import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE -import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT -import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.entity.Account @@ -31,16 +27,12 @@ import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Error -import com.keylesspalace.tusky.util.IOUtils import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success -import com.keylesspalace.tusky.util.getSampledBitmap import com.keylesspalace.tusky.util.randomAlphanumericString -import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo -import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -52,30 +44,26 @@ import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.OutputStream import javax.inject.Inject private const val HEADER_FILE_NAME = "header.png" private const val AVATAR_FILE_NAME = "avatar.png" -private const val TAG = "EditProfileViewModel" - class EditProfileViewModel @Inject constructor( private val mastodonApi: MastodonApi, - private val eventHub: EventHub + private val eventHub: EventHub, + private val application: Application ) : ViewModel() { val profileData = MutableLiveData>() - val avatarData = MutableLiveData>() - val headerData = MutableLiveData>() + val avatarData = MutableLiveData() + val headerData = MutableLiveData() val saveData = MutableLiveData>() val instanceData = MutableLiveData>() private var oldProfileData: Account? = null - private val disposeables = CompositeDisposable() + private val disposables = CompositeDisposable() fun obtainProfile() { if (profileData.value == null || profileData.value is Error) { @@ -92,70 +80,30 @@ class EditProfileViewModel @Inject constructor( profileData.postValue(Error()) } ) - .addTo(disposeables) + .addTo(disposables) } } - fun newAvatar(uri: Uri, context: Context) { - val cacheFile = getCacheFileForName(context, AVATAR_FILE_NAME) + fun getAvatarUri() = getCacheFileForName(AVATAR_FILE_NAME).toUri() - resizeImage(uri, context, AVATAR_SIZE, AVATAR_SIZE, cacheFile, avatarData) + fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri() + + fun newAvatarPicked() { + avatarData.value = getAvatarUri() } - fun newHeader(uri: Uri, context: Context) { - val cacheFile = getCacheFileForName(context, HEADER_FILE_NAME) - - resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData) + fun newHeaderPicked() { + headerData.value = getHeaderUri() } - private fun resizeImage( - uri: Uri, - context: Context, - resizeWidth: Int, - resizeHeight: Int, - cacheFile: File, - imageLiveData: MutableLiveData> - ) { - - Single.fromCallable { - val contentResolver = context.contentResolver - val sourceBitmap = getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight) - - if (sourceBitmap == null) { - throw Exception() - } - - // dont upscale image if its smaller than the desired size - val bitmap = - if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) { - sourceBitmap - } else { - Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true) - } - - if (!saveBitmapToFile(bitmap, cacheFile)) { - throw Exception() - } - - bitmap - }.subscribeOn(Schedulers.io()) - .subscribe( - { - imageLiveData.postValue(Success(it)) - }, - { - imageLiveData.postValue(Error()) - } - ) - .addTo(disposeables) - } - - fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List, context: Context) { + fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List) { if (saveData.value is Loading || profileData.value !is Success) { return } + saveData.value = Loading() + val displayName = if (oldProfileData?.displayName == newDisplayName) { null } else { @@ -174,15 +122,15 @@ class EditProfileViewModel @Inject constructor( newLocked.toString().toRequestBody(MultipartBody.FORM) } - val avatar = if (avatarData.value is Success && avatarData.value?.data != null) { - val avatarBody = getCacheFileForName(context, AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) + val avatar = if (avatarData.value != null) { + val avatarBody = getCacheFileForName(AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody) } else { null } - val header = if (headerData.value is Success && headerData.value?.data != null) { - val headerBody = getCacheFileForName(context, HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) + val header = if (headerData.value != null) { + val headerBody = getCacheFileForName(HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody) } else { null @@ -256,29 +204,12 @@ class EditProfileViewModel @Inject constructor( ) } - private fun getCacheFileForName(context: Context, filename: String): File { - return File(context.cacheDir, filename) - } - - private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean { - - val outputStream: OutputStream - - try { - outputStream = FileOutputStream(file) - } catch (e: FileNotFoundException) { - Log.w(TAG, Log.getStackTraceString(e)) - return false - } - - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) - IOUtils.closeQuietly(outputStream) - - return true + private fun getCacheFileForName(filename: String): File { + return File(application.cacheDir, filename) } override fun onCleared() { - disposeables.dispose() + disposables.dispose() } fun obtainInstance() { @@ -293,7 +224,7 @@ class EditProfileViewModel @Inject constructor( instanceData.postValue(Error()) } ) - .addTo(disposeables) + .addTo(disposables) } } } diff --git a/app/src/main/res/drawable-v26/launcher_shadow_gradient.xml b/app/src/main/res/color-v24/launcher_shadow_gradient.xml similarity index 100% rename from app/src/main/res/drawable-v26/launcher_shadow_gradient.xml rename to app/src/main/res/color-v24/launcher_shadow_gradient.xml diff --git a/app/src/main/res/drawable-hdpi/chinwag_splash.png b/app/src/main/res/drawable-hdpi/chinwag_splash.png deleted file mode 100644 index 3fb4925e..00000000 Binary files a/app/src/main/res/drawable-hdpi/chinwag_splash.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/splash.png b/app/src/main/res/drawable-hdpi/splash.png deleted file mode 100644 index 965c97d3..00000000 Binary files a/app/src/main/res/drawable-hdpi/splash.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/chinwag_splash.png b/app/src/main/res/drawable-mdpi/chinwag_splash.png deleted file mode 100644 index e966d213..00000000 Binary files a/app/src/main/res/drawable-mdpi/chinwag_splash.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/splash.png b/app/src/main/res/drawable-mdpi/splash.png deleted file mode 100644 index 019fc271..00000000 Binary files a/app/src/main/res/drawable-mdpi/splash.png and /dev/null differ diff --git a/app/src/main/res/drawable-v26/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 96% rename from app/src/main/res/drawable-v26/ic_launcher_foreground.xml rename to app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 45f4cf69..0a1e7d8e 100644 --- a/app/src/main/res/drawable-v26/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -1,6 +1,6 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 00000000..361ebe25 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..bf1bcaa2 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml new file mode 100644 index 00000000..c9f4c937 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index ec4e0910..78865410 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -331,7 +331,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="6dp" - android:text="@string/title_statuses" + android:text="@string/title_posts" android:textColor="@color/account_tab_font_color" android:textSize="?attr/status_text_medium" /> diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index e441af02..2db42a3f 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -313,10 +313,10 @@ android:layout_width="36dp" android:layout_height="36dp" android:layout_marginEnd="4dp" - android:contentDescription="@string/action_schedule_toot" + android:contentDescription="@string/action_schedule_post" android:padding="4dp" app:srcCompat="@drawable/ic_access_time" - app:tooltipText="@string/action_schedule_toot" /> + app:tooltipText="@string/action_schedule_post" /> + tools:context=".EditProfileActivity"> - - - - + tools:context="com.keylesspalace.tusky.components.login.LoginActivity"> - - - - - - - - - diff --git a/app/src/main/res/layout/activity_scheduled_toot.xml b/app/src/main/res/layout/activity_scheduled_status.xml similarity index 94% rename from app/src/main/res/layout/activity_scheduled_toot.xml rename to app/src/main/res/layout/activity_scheduled_status.xml index 6e2217c6..71ba5e65 100644 --- a/app/src/main/res/layout/activity_scheduled_toot.xml +++ b/app/src/main/res/layout/activity_scheduled_status.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".components.scheduled.ScheduledTootActivity"> + tools:context=".components.scheduled.ScheduledStatusActivity"> - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_tag.xml b/app/src/main/res/layout/activity_view_tag.xml deleted file mode 100644 index e9833824..00000000 --- a/app/src/main/res/layout/activity_view_tag.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_poll.xml b/app/src/main/res/layout/dialog_add_poll.xml index ed8abc07..4ba72250 100644 --- a/app/src/main/res/layout/dialog_add_poll.xml +++ b/app/src/main/res/layout/dialog_add_poll.xml @@ -37,7 +37,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:entries="@array/poll_duration_names" app:layout_constraintBottom_toBottomOf="@id/addChoiceButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/addChoiceButton" diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml index 1d6eab67..4a91a785 100644 --- a/app/src/main/res/layout/item_conversation.xml +++ b/app/src/main/res/layout/item_conversation.xml @@ -154,7 +154,7 @@ android:visibility="gone" app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintTop_toBottomOf="@id/status_content_warning_description" - tools:text="@string/status_content_warning_show_more" + tools:text="@string/post_content_warning_show_more" tools:visibility="visible" /> + tools:text="Some post content. May be very long." /> diff --git a/app/src/main/res/layout/item_report_status.xml b/app/src/main/res/layout/item_report_status.xml index 56a2dea9..54197b01 100644 --- a/app/src/main/res/layout/item_report_status.xml +++ b/app/src/main/res/layout/item_report_status.xml @@ -46,7 +46,7 @@ android:visibility="gone" app:layout_constraintStart_toStartOf="@id/guideBegin" app:layout_constraintTop_toBottomOf="@id/statusContentWarningDescription" - tools:text="@string/status_content_warning_show_more" + tools:text="@string/post_content_warning_show_more" tools:visibility="visible" /> + tools:text="@string/post_content_warning_show_more" /> @@ -130,7 +130,7 @@ android:paddingRight="16dp" android:paddingBottom="4dp" android:textAllCaps="true" - tools:text="@string/status_content_show_less" + tools:text="@string/post_content_show_less" style="@style/TuskyButton.Outlined" android:textSize="?attr/status_text_medium" android:visibility="gone" /> diff --git a/app/src/main/res/layout/login_webview.xml b/app/src/main/res/layout/login_webview.xml new file mode 100644 index 00000000..22d7f3bd --- /dev/null +++ b/app/src/main/res/layout/login_webview.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml index d25bcdc1..8e0dc242 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -6,6 +6,10 @@ android:title="@string/action_open_in_web" app:showAsAction="never" /> + + diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml index c73b7e9f..4a13b45d 100644 --- a/app/src/main/res/menu/status_more.xml +++ b/app/src/main/res/menu/status_more.xml @@ -5,11 +5,11 @@ android:title="@string/action_share"> + android:id="@+id/post_share_link" + android:title="@string/post_share_link" /> + android:id="@+id/post_share_content" + android:title="@string/post_share_content" /> + android:id="@+id/post_share_link" + android:title="@string/post_share_link" /> + android:id="@+id/post_share_content" + android:title="@string/post_share_content" /> \ No newline at end of file diff --git a/app/src/main/res/menu/view_thread_toolbar.xml b/app/src/main/res/menu/view_thread_toolbar.xml index 456f2094..65577d20 100644 --- a/app/src/main/res/menu/view_thread_toolbar.xml +++ b/app/src/main/res/menu/view_thread_toolbar.xml @@ -9,7 +9,7 @@ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index fe5f1260..52c036e5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,9 +1,9 @@ - + + android:drawable="@drawable/ic_launcher" /> \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a565a2c8..28419a2a 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -18,17 +18,17 @@ التصريح لازم للإحتفاظ بالوسائط. لا يمكنك إرفاق كلا من الصور والفيديوهات في نفس المنشور في آن واحد. اخفقت عملية الرفع. - خطأ عند إرسال التبويق. + خطأ عند إرسال المنشور. الرئيسي الاشعارات المحلي الفدرالي الرسائل المباشرة الألسنة - تبويق - المنشورات - التبويقات والردود - المدبّسة + خيط + المنشورات + التبويقات والردود + المدبّسة المتابَعون المتابِعون المفضلة @@ -38,19 +38,19 @@ عدل ملفك التعريفي المسودات الرّخص - \@%s - شارَكَه %s - محتوى حساس - وسائط مخفية - اضغط للعرض - اعرض المزيد - اعرض أقل - توسيع - تصغير + \@%s + شارَكَه %s + محتوى حساس + وسائط مخفية + اضغط للعرض + اعرض المزيد + اعرض أقل + توسيع + تصغير لا شيء هنا. لا يوجد شيء هنا. إسحب إلى أسفل للإنعاش! - شارَك %s تبويقك - أعجِب %s بتبويقك + شارَك %s منشورك + أعجِب %s بمنشورك %s يتبعك أبلغ عن @%s تعليقات إضافية؟ @@ -102,7 +102,7 @@ رفض البحث المسودات - كيفية عرض التبويق + كيفية عرض المنشور تحذير عن المحتوى لوحة مفاتيح الإيموجي إضافة لسان @@ -118,14 +118,14 @@ إنسخ الرابط إفتحه كـ %s شاركه كـ… - شارك رابط التبويق مع… - شارك التبويق على… + شارك رابط التبويق مع… + شارك التبويق على… شارك الوسيط مع… تم إرساله! تم فك الحجب عن الحساب لم يعد الحساب مكتومًا - تم إرساله! - تم إرسال الرد بنجاح. + تم إرساله! + تم إرسال الرد بنجاح. أي مثيل خادم؟ ما الجديد؟ تحذير عن المحتوى @@ -150,7 +150,7 @@ تنزيل هل تريد رفض طلب المتابعة؟ هل تود إلغاء متابعة هذا الحساب؟ - هل تريد حذف هذا التبويق؟ + هل تريد حذف هذا التبويق؟ للعامة: ينشر على الخيوط العمومية غير مدرج: لا يُعرَض على الخيوط العمومية لمتابعيك فقط: يُنشر إلى متابعيك فقط @@ -179,8 +179,8 @@ إخفاء زر المتابعة أثناء تمرير الصفحة إخفاء زر التحرير عند التمرير اللغة - تصفية الخيوط - الألسنة + تصفية الخيوط + الألسنة عرض الترقيات عرض الردود إظهار معاينات الوسائط @@ -196,20 +196,20 @@ للعامة غير مدرج للمُتابِعين فقط - حجم الخط - صغير جدا - صغير - متوسط - عريض - عريض جدا + حجم الخط + صغير جدا + صغير + متوسط + عريض + عريض جدا إشارات جديدة الإخطارات عندما يشار إليك متابِعون جدد إخطارات عند تلقي متابعين جدد الترقيات - إخطارات عندما يقوم أحدهم بترقية تبويقاتي + إخطارات عندما يقوم أحدهم بمشاركة منشوراتي المفضلة - الإشعار عندما يقوم أحدهم بإضافة تبويقاتك إلى مفضلاته + الإشعار عندما يقوم أحدهم بإضافة منشوراتك إلى مفضلاته %s أشار إليك %1$s, %2$s, %3$s و %4$d آخرون %1$s, %2$s, و %3$s @@ -237,10 +237,10 @@ تقارير الأخطاء و طلبات التحسينات على :\n https://git.chinwag.org/chinwag/chinwag-android/issues الملف الشخصي لتوسكي - شارك محتوى التبويق - شارك الرابط إلى التبويق - صور - فيديو + شارك محتوى التبويق + شارك الرابط إلى التبويق + صور + فيديو طلب متابعة في %dy @@ -291,11 +291,11 @@ تجميد الحساب يتطلب منك قبول طلبات المتابَعة يدويا هل تود الإحتفاظ بالمسودة ؟ - جارٍ إرسال التبويق… - خطأ أثناء عملية إرسال التبويق - إرسال التبويقات - أُلغيَ الإرسال - تم الاحتفاظ بنسخة مِن التبويق في مسوداتك + جارٍ إرسال التبويق… + خطأ أثناء عملية إرسال التبويق + إرسال التبويقات + أُلغيَ الإرسال + تم الاحتفاظ بنسخة مِن التبويق في مسوداتك حرر لا يحتوي مثيل خادومكم %s على أية حزمة إيموجي مخصصة تم نسخه إلى الحافظة @@ -303,8 +303,8 @@ الإفتراضي في النظام يجب عليك أولا تنزيل حزمة الإيموجي هذه البحث جارٍ … - توسيع/طي كافة المنشورات - افتح التبويق + توسيع/طي كافة المنشورات + افتح التبويق مطلوب إعادة تشغيل التطبيق إعادة تشغيل توسكي مطلوبة قصد تفعيل التعديلات لاحقًا @@ -342,10 +342,10 @@ لقد بلغت الحد الأقصى مِن الألسنة %1$d لقد بلغت الحد الأقصى مِن الألسنة %1$d - الوسائط: %s - تحذير عن المحتوى: %s - مِن دون وصف - أعاد تدوينه + الوسائط: %s + تحذير عن المحتوى: %s + مِن دون وصف + أعاد تدوينه للعامة غير مُدرَج المتابِعون @@ -356,13 +356,13 @@ افتح الوسيط #%d نزّل الوسائط جارٍ تنزيل الوسائط - هل تريد حذف وإعادة صياغة هذا التبويق؟ - تم تفضيله + هل تريد حذف وإعادة صياغة هذا التبويق؟ + تم تفضيله وسم بدون # مسح عامل تصفية طَبِّق - تحرير تبويق + تحرير منشور كتابة هل تريد حقا مسح كافة إشعاراتك؟ ينتهي في %s @@ -445,7 +445,7 @@ تعليقات إضافية أعد تحويله إلى %s فشل الابلاغ - فشلت عملية جلب المنشورات + فشلت عملية جلب المنشورات اعرض مصفاة الإشعارات الكلمة كاملة استطلاع رأي بالخيارات: %1$s, %2$s, %3$s, %4$s; %5$s @@ -455,7 +455,7 @@ الحسابات فشل البحث إضافة استطلاع رأي - افتح دائما التبويقات التي تحتوي على محتوى حساس + افتح دائما المنشورات التي تحتوي على محتوى حساس استطلاع رأي 5 دقائق 30 دقيقة @@ -470,21 +470,21 @@ تعديل عندما تكون الكلمة أو العبارة أبجدية رقمية فقط ، فلن يتم تطبيقها إلا إذا كانت مطابقة للكلمة بأكملها %1$s • %2$s - التبويقات المبَرمَجة + التبويقات المبَرمَجة تعديل - التبويقات المبَرمَجة - برمجة تبويق + التبويقات المبَرمَجة + برمجة تبويق صفّر خطأ أثناء البحث عن منشور %s الفواصل المرجعية أضفه إلى الفواصل المرجعية الفواصل المرجعية مدعوم بِـ Tusky - أضيف إلى الفواصل المرجعية + أضيف إلى الفواصل المرجعية اختر قائمة القائمة ليس لديك أية مسودات. - ليس لديك أية منشورات مُبرمَجة للنشر. + ليس لديك أية منشورات مُبرمَجة للنشر. يجب أن يكون حجم الملفات الصوتية أقل مِن 40 ميغابايت. تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق. تمكين حركات السحب للانتقال بين الألسنة @@ -520,23 +520,23 @@ إخفاء عنوان شريط الأدوات العلوي الاعلانات أتريد حقا حذف القائمة %s؟ - فشلت عملية إرسال التبويق! + فشلت عملية إرسال التبويق! حُذفَت المسودة اشترك إلغاء الإشتراك نشر %s للتوّ احذف المحادثة هل تريد حذف هذه المحادثة؟ - تبويقات جديدة - مرفقات + منشورات جديدة + مرفقات المدة لا توجد إعلانات. ملاحظتك الخاصة عن هذا الحساب تم حفظها! راجع الإشعارات - صوت - لقد حُذِف التبويق الذي حررت من أجله مسودة الرد - شخص ما أنا مشترك في حسابه قد نشر تبويقا جديدا + صوت + لقد حُذِف التبويق الذي حررت من أجله مسودة الرد + شخص ما أنا مشترك في حسابه قد نشر منشورا جديدا حرّك الإيموجيات المخصصة إخفاء الإحصائيات الكمية عن المنشورات إخفاء الإحصائيات الكمية عن الملفات التعريفية @@ -545,4 +545,11 @@ فشل في تحميل معلومات الرد غير محددة وضع حد لإشعارات الخيط الزمني + 14 يومًا + 30 يومًا + 60 يومًا + 90 يومًا + 180 يومًا + 365 يومًا + تحرير منشور \ No newline at end of file diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index b6d317b5..c754aef8 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -30,14 +30,14 @@ ⴽⴽⴻⵙ ⵉⵔⴻⵡⵡⴰⵢⴻⵏ ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ - ⵉⵛⵛⴰⵔⴻⵏ + ⵉⵛⵛⴰⵔⴻⵏ ⴰⵏⵜⴰ ⵝⵓⵎⵎⴰⵏⵜ\? ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ ⵔⵏⵓ ⴰⵎⵉⴹⴰⵏ ⴸⴰⵛⵓ ⵉⴳⴻⵍⵍⴰⵏ ⴸ ⴰⵎⴰⵢⵏⵓⵝ\? - ⵙⵖⵉⵡⴻⵙ ⵝⵉⵊⴻⵡⵡⵉⵇⵝⴰ - ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ - ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ + ⵙⵖⵉⵡⴻⵙ ⵝⵉⵊⴻⵡⵡⵉⵇⵝⴰ + ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ + ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ ⵝⵉⵛⵔⴰⴹ ⵝⵉⵛⵔⴰⴹ \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index a3db937b..a6ca7a65 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,7 +1,7 @@ Публикация - Публикацията, на която сте изготвили отговор, е премахната + Публикацията, на която сте изготвили отговор, е премахната 1 час 30 минути 5 минути @@ -14,7 +14,7 @@ Акаунти Акаунтът е от друг сървър. Да изпратите ли и там анонимно копие на доклада\? Докладът ще бъде изпратен на модератора на вашия сървър. Можете да предоставите обяснение защо докладвате този акаунт по-долу: - Извличането на състояния бе неуспешно + Извличането на състояния бе неуспешно Докладването бе неуспешно Препращане към %s Допълнителни коментари @@ -70,12 +70,12 @@ Директно Последователи Публично - Отметнато - Поставено в любими - Реблог - Без описание - Предупреждение за съдържание: %s - Мултимедия: %s + Отметнато + Поставено в любими + Реблог + Без описание + Предупреждение за съдържание: %s + Мултимедия: %s достигнати са максималните %1$d раздела @@ -118,19 +118,19 @@ По-късно Ще трябва да рестартирате Tusky, за да приложите тези промени Изисква се рестартиране на приложението - Отваряне на публикация - Разгъване/свиване на всички състояния + Отваряне на публикация + Разгъване/свиване на всички състояния Извършва се търсене… По подразбиране от системата Стил на емоджи Копирано в клипборда Инстанцията ви %s няма персонализирани емоджита Композиране - Копие от публикацията е запазено във вашите чернови - Изпращането е отменено - Изпращане на публикации - Грешка при изпращане на публикация - Изпращане на публикация… + Копие от публикацията е запазено във вашите чернови + Изпращането е отменено + Изпращане на публикации + Грешка при изпращане на публикация + Изпращане на публикация… Запазване на чернова\? Изисква ръчно одобряване на последователи Заключване на акаунт @@ -183,12 +183,12 @@ след %dг след %dд Заявено последване - Прикачени файлове - Аудио - Видео - Изображения - Споделяне на връзка към публикация - Споделяне на съдържание на публикация + Прикачени файлове + Аудио + Видео + Изображения + Споделяне на връзка към публикация + Споделяне на съдържание на публикация Профилът на Tusky Доклади за грешки и заявки за функции: \n https://github.com/tuskyapp/Tusky/issues @@ -213,10 +213,10 @@ Известия, когато публикациите ви бъдат означени като любими Любими Известия, когато публикациите ви се споделят - Най-малък + Най-малък Скрито - Раздели - Филтриране на емисия + Раздели + Филтриране на емисия Анимиране на персонализирани емоджита Показване на цветни градиенти за скрита мултимедия Анимиране на GIF аватари @@ -256,26 +256,26 @@ Блокиране на @%s\? Скриване на целия домейн Сигурни ли сте, че искате да блокирате всички от %s\? Няма да виждате съдържание от този домейн в нито една публична емисия или във вашите известия. Последователите ви от този домейн ще бъдат премахнати. - Изтриване и преработване на тази публикация\? - Изтриване на тази публикация\? + Изтриване и преработване на тази публикация\? + Изтриване на тази публикация\? Отследване на този акаунт\? Отмяна на заявката за последване\? Изтегляне Качване… Завършване на мултимедийно качване - "Тук може да се въведе адресът или домейнът на която и да е инстанция, като mastodon.social, icosahedron.website, social.tchncs.de и <a href=\"https://instances.social\">други!</a> + Тук може да се въведе адресът или домейнът на която и да е инстанция, като mastodon.social, icosahedron.website, social.tchncs.de и <a href=\"https://instances.social\">други!</a> \n \nАко все още нямате акаунт, можете да въведете името на инстанцията, към който искате да се присъедините, и да създадете акаунт там. \n \nИнстанцията е единично място, където се хоства акаунтът ви, но можете лесно да комуникирате и да следвате хора в други инстанции, сякаш сте на същия сайт. \n -\nПовече информация можете да намерите на <a href=\"https://joinmastodon.org\">joinmastodon.org</a>. "more! - \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to - join and create an account there.\n\nAn instance is a single place where your account is - hosted, but you can easily communicate with and follow folks on other instances as though - you were on the same site. - \n\nMore info can be found at joinmastodon.org. - +\nПовече информация можете да намерите на <a href=\"https://joinmastodon.org\">joinmastodon.org</a>. more! +\n +\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there. +\n +\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site. +\n +\nMore info can be found at joinmastodon.org. Свързване… Какво е инстанция\? Заглавна част @@ -288,15 +288,15 @@ Предупреждение за съдържание Какво се случва\? Коя инстанция\? - Отговорът е изпратен успешно. - Изпратено! + Отговорът е изпратен успешно. + Изпратено! %s е разкрит Потребителят е раззаглушен Потребителят е деблокиран Изпратено! Споделяне на мултимедия в… - Споделяне на публикация в… - Споделяне на URL адреса на публикацията в… + Споделяне на публикация в… + Споделяне на URL адреса на публикацията в… Теглене на мултимедия Изтегляне на мултимедия Споделяне като … @@ -315,11 +315,11 @@ Връзки Добавяне на раздел Нулиране - Планиране на публикация + Планиране на публикация Емоджи клавиатура Предупреждение за съдържание Видимост на публикация - Планирани публикации + Планирани публикации Чернови Търсене Отхвърляне @@ -390,18 +390,18 @@ %s сподели вашата публикация Нищо тук. Дръпнете надолу, за да опресните! Нищо тук. - Свиване - Разгъване - Покажи по-малко - Покажи повече - Щракнете за преглед - Мултимедията е скрита - Деликатно съдържание - %s сподели - \@%s + Свиване + Разгъване + Покажи по-малко + Покажи повече + Щракнете за преглед + Мултимедията е скрита + Деликатно съдържание + %s сподели + \@%s Лицензи Оповестявания - Планирани публикации + Планирани публикации Чернови Редакция на профила ви Заявки за последване @@ -412,9 +412,9 @@ Любими Последователи Последвани - Закачени - С отговори - Публикации + Закачени + С отговори + Публикации Раздели Директни съобщения Локално @@ -442,7 +442,7 @@ Възникна грешка. Черновата е изтрита Неуспешно зареждане на информация за отговор - Тази публикация не успя да се изпрати! + Тази публикация не успя да се изпрати! Наистина ли искате да изтриете списъка %s\? Не можете да качите повече от %1$d мултимедийни прикачени файлове. @@ -466,7 +466,7 @@ Показване на визуализации на връзки в емисии Mastodon има минимален интервал за планиране от 5 минути. Няма оповестявания. - Нямате планирани състояния. + Нямате планирани състояния. Нямате чернови. Грешка при търсенето на публикация %s Редакция @@ -484,12 +484,12 @@ Нови последователи Известия за нови споменавания Нови споменавания - Най-голям - Голям - Среден - Малък + Най-голям + Голям + Среден + Малък Скрито: Не се показва в публични емисии - Размер на текста на състоянието + Размер на текста на състоянието Само за последователи Скрито Публично diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 33db1b27..79ecc214 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -7,11 +7,11 @@ অনুগামিবৃন্দ অতালিকাভুক্ত সর্বজনীন - পছন্দ - আবার ব্লগ - বর্ণনা নাই - সতর্কবার্তা: %s - মিডিয়া: %s + পছন্দ + আবার ব্লগ + বর্ণনা নাই + সতর্কবার্তা: %s + মিডিয়া: %s সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে @@ -42,8 +42,8 @@ পরবর্তীতে এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে অ্যাপ্লিকেশন পুনরায় আরম্ভ করা প্রয়োজন - টুট খুলুন - সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন + টুট খুলুন + সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন অনুসন্ধান করা হচ্ছে … আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে সিস্টেমের ডিফল্ট @@ -51,11 +51,11 @@ ক্লিপবোর্ডে অনুলিপি করা হয়েছে আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই রচনা - টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে - পাঠানো বাতিল - টুট পাঠানো হচ্ছে - টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে - টুট পাঠানো হচ্ছে … + টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে + পাঠানো বাতিল + টুট পাঠানো হচ্ছে + টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে + টুট পাঠানো হচ্ছে … ড্রাফট সংরক্ষণ\? অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন অ্যাকাউন্ট লক করুন @@ -99,10 +99,10 @@ \'%dd এ\' \'%dy এ\' অনুরোধ অনুসরণ করুন - ভিডিও - চিত্রগুলি - টুট এর সাথে লিংক ভাগ করুন - টুট এর কন্টেন্ট ভাগ করুন + ভিডিও + চিত্রগুলি + টুট এর সাথে লিংক ভাগ করুন + টুট এর কন্টেন্ট ভাগ করুন টাস্কির প্রোফাইল বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: \nhttps://git.chinwag.org/chinwag/chinwag-android/issues @@ -122,12 +122,12 @@ নতুন অনুসরণকারী নতুন উল্লেখ সম্পর্কে বিজ্ঞপ্তি নতুন উল্লেখসমূহ - বৃহত্তম - বড় - মাঝারি - ছোট - কনিষ্ঠ - স্থিতি টেক্সট আকার + বৃহত্তম + বড় + মাঝারি + ছোট + কনিষ্ঠ + স্থিতি টেক্সট আকার শুধুমাত্র অনুগামিবৃন্দ অতালিকাভুক্ত সর্বজনীন @@ -143,8 +143,8 @@ মিডিয়া পূর্বরূপ ডাউনলোড করুন উত্তর প্রদর্শন করুন সমর্থন দেখান - ট্যাবগুলি - টাইমলাইন ফিল্টারিং + ট্যাবগুলি + টাইমলাইন ফিল্টারিং GIF অবতার অ্যানিমেশন করুন বট জন্য সূচক প্রদর্শন করুন ভাষা @@ -176,26 +176,30 @@ শুধুমাত্র অনুসরণকারীদের: শুধুমাত্র অনুসরণকারীদের পোস্ট করুন তালিকাভুক্ত নয়: সর্বজনীন সময়সূচীগুলিতে দেখাবেন না সর্বজনীন: পাবলিক টাইমলাইনে পোস্ট কর - এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\? - এই টুট টি মুছে ফেলবেন\? + এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\? + এই টুট টি মুছে ফেলবেন\? এই অ্যাকাউন্টটি অনুসরণ করবেন না\? অনুসরণ অনুরোধ প্রত্যাহার\? ডাউনলোড আপলোড হচ্ছে … মিডিয়া আপলোড সমাপ্ত করা হচ্ছে - "কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং <a href=\"https://instances.social\"> আরও! </a> + কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং <a href=\"https://instances.social\"> আরও! </a> \n \nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n \n +\n \nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n \n -\nআরো তথ্য <a href=\"https://joinmastodon.org\"> joinmastodon.org </a> এ পাওয়া যেতে পারে। "more! - \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to - join and create an account there.\n\nAn instance is a single place where your account is - hosted, but you can easily communicate with and follow folks on other instances as though - you were on the same site. - \n\nMore info can be found at joinmastodon.org. - +\n +\nআরো তথ্য <a href=\"https://joinmastodon.org\"> joinmastodon.org </a> এ পাওয়া যেতে পারে। more! +\n +\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there. +\n +\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site. +\n +\nMore info can be found at joinmastodon.org. সংযুক্ত হচ্ছে … ইনস্ট্যান্স কি\? হেডার @@ -208,14 +212,14 @@ সতর্কবার্তা কি হচ্ছে\? কোন ইনস্ট্যান্স\? - উত্তর সফলভাবে পাঠানো হয়েছে। - পাঠানো হয়েছে! + উত্তর সফলভাবে পাঠানো হয়েছে। + পাঠানো হয়েছে! ব্যবহারকারী সশব্দ ব্যবহারকারী অবরোধ মুক্ত পাঠানো হয়েছে! মিডিয়া শেয়ার করুন … - টুট ভাগ করুন … - টুট URL ভাগ করুন … + টুট ভাগ করুন … + টুট URL ভাগ করুন … মিডিয়া ডাউনলোড করা হচ্ছে মিডিয়া ডাউনলোড করুন হিসাবে ভাগ করুন … @@ -289,13 +293,13 @@ রিপোর্ট @%s এখানে কিছু নেই. রিফ্রেশ করতে নিচে টানুন! এখানে কিছুই নেই। - বন্ধ - বিস্তৃত - প্রদর্শন কম - আরও দেখাও - দেখার জন্য ক্লিক করুন - মিডিয়া লুকানো - সংবেদনশীল কন্টেন্ট + বন্ধ + বিস্তৃত + প্রদর্শন কম + আরও দেখাও + দেখার জন্য ক্লিক করুন + মিডিয়া লুকানো + সংবেদনশীল কন্টেন্ট লাইসেন্সগুলি খসড়া আপনার প্রোফাইল সম্পাদনা করুন @@ -306,8 +310,8 @@ %dমা %dঘ %dব - %s বুস্টকৃত - \@%s + %s বুস্টকৃত + \@%s বিজ্ঞপ্তি লুকাও \'%s থেকে বিজ্ঞপ্তি বন্ধ করো\' \'%s থেকে বিজ্ঞপ্তি বন্ধ করো\' @@ -332,20 +336,20 @@ মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। অডিও ফাইলগুলি অবশ্যই ৪০MB এর চেয়ে কম হওয়া উচিত। তোমার কোনো খসড়া নেই। - তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। + তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। তালিকা তালিকা নির্বাচন করো - বুকমার্ককৃত + বুকমার্ককৃত বুকমার্কগুলি বুকমার্ক বুকমার্কগুলি টাস্কি দ্বারা চালিত \'%s পোস্ট অনুসন্ধানে ত্রুটি\' রিসেট - নির্ধারিত টুট - নির্ধারিত টুটগুলি + নির্ধারিত টুট + নির্ধারিত টুটগুলি সম্পাদন - নির্ধারিত টুটগুলি + নির্ধারিত টুটগুলি সম্পাদন পছন্দ %d একাধিক পছন্দ @@ -373,7 +377,7 @@ গোপন ডোমেইনগুলি এই একাউন্ট তা একটি অন্য সার্ভারের। রিপোর্ট এর সঙ্গে একটি বেনামি কপি ওখানে পাঠাতে চান\? রিপোর্ট আপনার সার্ভার মডারেটরে পাঠানো হবে। আপনি নীচের এই অ্যাকাউন্টটি কেন প্রতিবেদন করছেন তা ব্যাখ্যা করতে পারেন: - স্টেটাসগুলি আনতে ব্যর্থ + স্টেটাসগুলি আনতে ব্যর্থ রিপোর্ট করতে ব্যর্থ হয়েছে \'%s এ ফরওয়ার্ড করুন\' অতিরিক্ত মন্তব্যগুলি @@ -386,8 +390,7 @@ ভোট বন্ধ \'%s এ শেষ হবে\' - - %1$s • %2$s + %1$s • %2$s ছবি %s এর জন্য ক্রিয়া আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\? রচনা @@ -400,9 +403,9 @@ প্রিয়গুলো অনুগামিবৃন্দ অনুসরণ - পিন করা - উত্তরের সাথে - পোস্টগুলি + পিন করা + উত্তরের সাথে + পোস্টগুলি টুট ট্যাবগুলি সরাসরি বার্তা @@ -447,12 +450,12 @@ সদস্যতা আছে এমন একজন টুট দিয়েছে কোনো ঘোষণা নেই। যদিও তোমার অ্যাকাউন্ট রুদ্ধকৃত না, %1$s রা ভেবেছে এই অ্যাকাউন্টগুলোর অনুসরণ অনুরোধ তোমার পরীক্ষা করা উচিত। - যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে + যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। - টুট পাঠাতে ব্যর্থ! + টুট পাঠাতে ব্যর্থ! উত্তরের তথ্য আনতে ব্যর্থ সংরক্ষিত! অবতারে পরিসংখ্যান লুকাও @@ -505,8 +508,8 @@ %s বুস্ট %1$s স্থানান্তরিত হয়েছে এখানে: - সংযুক্তি - শব্দ + সংযুক্তি + শব্দ %dটি নতুন ক্রিয়া %dটি নতুন ক্রিয়া diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 3de2ffe3..92390c48 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -26,9 +26,9 @@ সরাসরি বার্তা ট্যাবগুলি টুট - পোস্টগুলি - উত্তরের সাথে - পিন করা + পোস্টগুলি + উত্তরের সাথে + পিন করা অনুসরণ অনুগামিবৃন্দ প্রিয়গুলো @@ -38,15 +38,15 @@ আপনার প্রোফাইল সম্পাদনা করুন খসড়াগুলো লাইসেন্সগুলি - \@%s - %s সমর্থন দিয়েছে - সংবেদনশীল কন্টেন্ট - মিডিয়া লুকানো - দেখার জন্য ক্লিক করুন - আরও দেখাও - প্রদর্শন কম - বিস্তৃত - বন্ধ + \@%s + %s সমর্থন দিয়েছে + সংবেদনশীল কন্টেন্ট + মিডিয়া লুকানো + দেখার জন্য ক্লিক করুন + আরও দেখাও + প্রদর্শন কম + বিস্তৃত + বন্ধ এখানে কিছুই নেই। এখানে কিছু নেই. রিফ্রেশ করতে নিচে টানুন! %s সমর্থন দিয়েছে @@ -123,14 +123,14 @@ হিসাবে ভাগ করুন … মিডিয়া ডাউনলোড করুন মিডিয়া ডাউনলোড করা হচ্ছে - টুট URL ভাগ করুন … - টুট ভাগ করুন … + টুট URL ভাগ করুন … + টুট ভাগ করুন … মিডিয়া শেয়ার করুন … পাঠানো হয়েছে! ব্যবহারকারী অবরোধ মুক্ত ব্যবহারকারী সশব্দ - পাঠানো হয়েছে! - উত্তর সফলভাবে পাঠানো হয়েছে। + পাঠানো হয়েছে! + উত্তর সফলভাবে পাঠানো হয়েছে। কোন ইনস্ট্যান্স\? কি হচ্ছে\? সতর্কবার্তা @@ -155,8 +155,8 @@ ডাউনলোড অনুসরণ অনুরোধ প্রত্যাহার\? এই অ্যাকাউন্টটি অনুসরণ করবেন না\? - এই টুট টি মুছে ফেলবেন\? - এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\? + এই টুট টি মুছে ফেলবেন\? + এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\? সর্বজনীন: পাবলিক টাইমলাইনে পোস্ট কর তালিকাভুক্ত নয়: সর্বজনীন সময়সূচীগুলিতে দেখাবেন না শুধুমাত্র অনুসরণকারীদের: শুধুমাত্র অনুসরণকারীদের পোস্ট করুন @@ -188,8 +188,8 @@ ভাষা বট জন্য সূচক প্রদর্শন করুন GIF অবতার অ্যানিমেশন করুন - টাইমলাইন ফিল্টারিং - ট্যাবগুলি + টাইমলাইন ফিল্টারিং + ট্যাবগুলি সমর্থন দেখান উত্তর প্রদর্শন করুন মিডিয়া পূর্বরূপ ডাউনলোড করুন @@ -205,12 +205,12 @@ সর্বজনীন অতালিকাভুক্ত শুধুমাত্র অনুগামিবৃন্দ - স্থিতি টেক্সট আকার - কনিষ্ঠ - ছোট - মাঝারি - বড় - বৃহত্তম + স্থিতি টেক্সট আকার + কনিষ্ঠ + ছোট + মাঝারি + বড় + বৃহত্তম নতুন উল্লেখসমূহ নতুন উল্লেখ সম্পর্কে বিজ্ঞপ্তি নতুন অনুসরণকারী @@ -243,10 +243,10 @@ বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: \nhttps://git.chinwag.org/chinwag/chinwag-android/issues টাস্কির প্রোফাইল - টুট এর কন্টেন্ট ভাগ করুন - টুট এর সাথে লিংক ভাগ করুন - চিত্রগুলি - ভিডিও + টুট এর কন্টেন্ট ভাগ করুন + টুট এর সাথে লিংক ভাগ করুন + চিত্রগুলি + ভিডিও অনুরোধ অনুসরণ করুন %dy এ @@ -297,11 +297,11 @@ অ্যাকাউন্ট লক করুন অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন ড্রাফট সংরক্ষণ\? - টুট পাঠানো হচ্ছে … - টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে - টুট পাঠানো হচ্ছে - পাঠানো বাতিল - টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে + টুট পাঠানো হচ্ছে … + টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে + টুট পাঠানো হচ্ছে + পাঠানো বাতিল + টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে রচনা আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই ক্লিপবোর্ডে অনুলিপি করা হয়েছে @@ -309,8 +309,8 @@ সিস্টেমের ডিফল্ট আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে অনুসন্ধান করা হচ্ছে … - সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন - টুট খুলুন + সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন + টুট খুলুন অ্যাপ্লিকেশন পুনরায় আরম্ভ করা প্রয়োজন এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে পরবর্তীতে @@ -352,11 +352,11 @@ সর্বাধিক %1$d টি ট্যাব পৌঁছেছে - মিডিয়া: %s - সতর্কবার্তা: %s - বর্ণনা নাই - আবার ব্লগ - পছন্দ + মিডিয়া: %s + সতর্কবার্তা: %s + বর্ণনা নাই + আবার ব্লগ + পছন্দ সর্বজনীন অতালিকাভুক্ত অনুগামিবৃন্দ @@ -384,7 +384,7 @@ অতিরিক্ত মন্তব্যগুলি %s এ ফরওয়ার্ড করুন রিপোর্ট করতে ব্যর্থ হয়েছে - স্টেটাসগুলি আনতে ব্যর্থ + স্টেটাসগুলি আনতে ব্যর্থ রিপোর্ট আপনার সার্ভার মডারেটরে পাঠানো হবে। আপনি নীচের এই অ্যাকাউন্টটি কেন প্রতিবেদন করছেন তা ব্যাখ্যা করতে পারেন: এই একাউন্ট তা একটি অন্য সার্ভারের। রিপোর্ট এর সঙ্গে একটি বেনামি কপি ওখানে পাঠাতে চান\? গোপন ডোমেইনগুলি @@ -412,10 +412,10 @@ একাধিক পছন্দ পছন্দ %d সম্পাদন - নির্ধারিত টুটগুলি + নির্ধারিত টুটগুলি সম্পাদন - নির্ধারিত টুটগুলি - নির্ধারিত টুট + নির্ধারিত টুটগুলি + নির্ধারিত টুট রিসেট টাস্কি দ্বারা চালিত %s পোস্ট অনুসন্ধানে ত্রুটি @@ -465,14 +465,14 @@ তালিকা নির্বাচন করো হ্যাশট্যাগ হ্যাশট্যাগ যোগ করো - বুকমার্ককৃত + বুকমার্ককৃত অনুসরণ রিকোয়েস্টের বিজ্ঞপ্তি সবচেয়ে শেষ সর্বপ্রথম মূল ন্যাভিগেশন জায়গা ট্যাবের মাঝে সোয়াইপ সংকেত চালু করো টাইমলাইনে লিঙ্ক প্রিভিউ দেখাও - তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। + তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। তোমার কোনো খসড়া নেই। মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। শীর্ষস্থানীয় সরঞ্জামদণ্ডের শিরোনামটি লুকান diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 668bfc51..27fa8122 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -21,7 +21,7 @@ Local Federació Toot - Posts + Posts Seguits Seguidors Preferits @@ -30,12 +30,12 @@ Peticions de seguiment Edita el perfil Esborranys - \@%s - %s tootejat - Contingut sensible - Fes clic per a visualitzar-lo - Mostra\'n més - Mostra\'n menys + \@%s + %s tootejat + Contingut sensible + Fes clic per a visualitzar-lo + Mostra\'n més + Mostra\'n menys No hi res aquí. Llisca avall per a actualitzar! %s ha impulsat el teu toot %s ha marcat com a preferit el teu toot @@ -83,8 +83,8 @@ Esborranys S\'està baixant %1$s Copia l\'enllaç - Comparteix l\'URL del toot a… - Comparteix el toot a… + Comparteix l\'URL del toot a… + Comparteix el toot a… Enviat! Usuari desblocat Usuari sense silenciar @@ -130,8 +130,8 @@ Navegador Pestanyes personalitzades del Chrome Amaga el botó de redacció en desplaçament - Filtre de la cronologia - Pestanyes + Filtre de la cronologia + Pestanyes Mostra els retoots Mostra les respostes Mostra les previsualitzacions @@ -140,7 +140,7 @@ Pública Sense llistar Només seguidors - Mida de text de l\'estat + Mida de text de l\'estat Mencions noves Notificacions sobre mencions noves Seguidors nous @@ -172,10 +172,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues
Perfil del Tusky - Comparteix el contingut del toot - Comparteix l\'enllaç al toot - Imatges - Vídeo + Comparteix el contingut del toot + Comparteix l\'enllaç al toot + Imatges + Vídeo en %d anys en %dd @@ -196,7 +196,7 @@ S\'ha produït un error en enviar el tut. Pestanyes Llicències - Amplia + Amplia Resposta ràpida Elimineu els preferits Preferències del compte @@ -206,8 +206,8 @@ Elimina l\'impuls S\'ha produït un error de connexió! Comproveu la connexió i torneu-ho a provar! Els fitxers de vídeo han de ser de mida menor de 40 MB. - Multimèdia amagada - Amaga + Multimèdia amagada + Amaga Estàs segur de tancar la sessió de %1$s\? Amaga els retoots Mostra els impulsos @@ -226,18 +226,18 @@ Comparteix com a… Baixa el fitxer Compartir la imatge a … - Enviat! + Enviat! S\'ha enviat la petició de seguiment - Amb respostes + Amb respostes Teclat d\'emojis Obrir el media #%d Obrir com %s S\'està Descarregant media - Resposta enviada correctament. + Resposta enviada correctament. Resposta … Revocar la petició de seguiment\? - Vols eliminar aquest toot\? - Esborrar i reescriure aquest toot\? + Vols eliminar aquest toot\? + Esborrar i reescriure aquest toot\? Finalització de les enquetes Tema Cronologia @@ -255,11 +255,11 @@ Port del proxy HTTP Marcar sempre els medias com a sensibles Error al sincronitzar els paràmetres - Petit - Petit - Mig - Gran - Més grand + Petit + Petit + Mig + Gran + Més grand Enquestes Converses Afegir un filtre @@ -297,11 +297,11 @@ Protegir el compte S\'haurà d\'admetre els seguidors manualment Guardar l\'esborrany\? - Enviant toot… - Error enviant el toot - Enviant toots - Envio anul·lat - Una copia del toot s\'ha guardat a esborranys + Enviant toot… + Error enviant el toot + Enviant toots + Envio anul·lat + Una copia del toot s\'ha guardat a esborranys Escriure La teva instància %s no te emojis personalitzats Copia al porta papers @@ -309,8 +309,8 @@ Sistema per defecte Hauràs de descarregar el joc d\'emojis Cercant… - Expandir/ocultar tots els estats - Obrir toot + Expandir/ocultar tots els estats + Obrir toot Cal reiniciar l\'aplicació Has de reiniciar l\'aplicació per tal d\'aplicar aquests canvis Més tard @@ -349,9 +349,9 @@ màxim de %1$d pestanyes aconseguides - Mèdia : %s - Sense descripció - Favorits + Mèdia : %s + Sense descripció + Favorits Públic Sense llistar Seguidors @@ -374,11 +374,11 @@ tancat L\'enquesta on has votat està tancada La enquesta que heu creat ha finalitzat - Advertència: %s - Toot fixat + Advertència: %s + Toot fixat Toot no fixat Fixar - Respost + Respost Accions per a la imatge %s Activar l\'animació dels GIF Dominis ocults @@ -396,7 +396,7 @@ Comentaris addicionals Reenviar a %s Report erroni - Error obtenint els estats + Error obtenint els estats L\'informe s\'enviarà al moderador del teu servidor. Pots afegir una explicació del motiu d\'aquest informe del compte a sota: Comptes Mostrar el filtre de les notificacions @@ -419,19 +419,19 @@ Múltiples tries Tria %d Preferits - Toots programats + Toots programats Preferit Edita Preferits - Toots programats - Programar el toot + Toots programats + Programar el toot Reiniciar Desenvolupat per Tusky - S\'ha afegit a les adreces d\'interès + S\'ha afegit a les adreces d\'interès Seleccionar la llista Llista S\'ha produït un error en cercar la publicació %s - No tens cap estat planificat. + No tens cap estat planificat. Els fitxers d\'àudio han de ser de mida menor de 40MB. No teniu cap esborrany. L\'interval mínim de planificació a Mastodon és de 5 minuts. @@ -485,18 +485,18 @@ falta %d dia falten %d dies - Adjuncions - Àudio + Adjuncions + Àudio Notificacions quan algú a qui esteu subscrit publica un tut nou Tuts nous emojis personalitzats animats algú a qui estic subscrit acaba de publicar un tut nou %s acaba de fer una publicació Avisos - S\'ha esborrat el tut del qual en vau fer un esborrany de resposta + S\'ha esborrat el tut del qual en vau fer un esborrany de resposta S\'ha eliminat l\'esborrany No s\'ha pogut carregar la informació de la resposta - No s\'ha pogut enviar aquest tut! + No s\'ha pogut enviar aquest tut! Segur que voleu esborrar la llista %s\? No podeu pujar més de %1$d adjunts multimèdia. diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index c5702f28..8dd86758 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -2,15 +2,15 @@ چی خەریکه ڕوودەدات؟ کام نموونە؟ - وەڵام دانەوە کە بە سەرکەوتوویی نێردرا. - ناردن! + وەڵام دانەوە کە بە سەرکەوتوویی نێردرا. + ناردن! %s نەشاراوە بەکارهێنەر نەگۆڕاو بەکارهێنەر بەربەست نەکراوە ناردن! هاوبەشکردنی میدیا بۆ… - هاوبەشی کردن بە توت بۆ… - هاوبەشکردنی توتی URL بۆ… + هاوبەشی کردن بە توت بۆ… + هاوبەشکردنی توتی URL بۆ… داگرتنی میدیا داگرتنی میدیا هاوبەش کردن وەک … @@ -29,11 +29,11 @@ بەستەرەکان زیادکردنی سەرخشت ڕیسێت کردن - خشتەی توت + خشتەی توت تەختەکلیلی ئیمۆجی ئاگاداری ناوەڕۆک بینینی توت - توتی خشتەکراو + توتی خشتەکراو ڕەشنووسەکان گەڕان ڕەتکردنەوە @@ -103,18 +103,18 @@ %s توتەکەتی بەرزکردەوە هیچ شتێک لێرە نیە ڕاکە خوارەوە بۆ نوێکردنەوە! هیچ شتێک لێرە نیە. - نوشتانەوە - فراوانکردن - کەمتر نیشان بدە - زیاتر پیشان بدە - کرتە بکە بۆ بینین - میدیا شاراوە - ناوەڕۆکی هەستیار - %s بەرزکرا - \@%s + نوشتانەوە + فراوانکردن + کەمتر نیشان بدە + زیاتر پیشان بدە + کرتە بکە بۆ بینین + میدیا شاراوە + ناوەڕۆکی هەستیار + %s بەرزکرا + \@%s مۆڵەتەکان ڕاگه یه نراوەکان - توتی خشتەکراو + توتی خشتەکراو دەستکاری پرۆفایلەکەت بکە بەدواداچونی داواکاریەکان بکە دۆمەینە شاراوەکان @@ -124,9 +124,9 @@ دڵخوازەکان شوێنکەوتوان بەدوادا - چەسپا - لەگەڵ وەڵامەکان - بابەتەکان + چەسپا + لەگەڵ وەڵامەکان + بابەتەکان توت سەرخشتەکان نامە ڕاستەوخۆکان @@ -163,8 +163,8 @@ داگرتنی پێشبینینی میدیا وەڵامدانەوەکان پیشان بدە پیشاندانی بەهێزکردنەکان - سەرخشتەکان - فلتەرکردنی تایملاین + سەرخشتەکان + فلتەرکردنی تایملاین نمرەی لاری ڕەنگاوڕەنگ نیشان بدە بۆ میدیای شاراوە وێنۆجکەی ئەنیمەی GIF نیشاندەر نیشاندەر بۆ بۆتەکان نیشان بدە @@ -204,8 +204,8 @@ بلۆککردنی @%s؟ شاردنەوەی هەموو دۆمەینەکە ئایا دڵنیایت لەوەی دەتەوێت هەموو %s بلۆک بکەیت؟ تۆ ناوەڕۆکێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان لە ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن. - ئەم دووانە بسڕەوە و دووبارە ڕەشنووس یان دەکەیتەوە؟ - ئەم توتە بسڕەوە؟ + ئەم دووانە بسڕەوە و دووبارە ڕەشنووس یان دەکەیتەوە؟ + ئەم توتە بسڕەوە؟ شوێن نەکەوتنی ئەم هەژمارە؟ داواکاری بەدوادا چوەکان هەڵوەشانەوە؟ داگرتن @@ -256,7 +256,7 @@ نیشاندانی پێشاندانی بەستەر لە هێڵی کات ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک. هیچ ڕاگه یه نراوێک له بەرده رنه کەون. - هیچ بارێکی خشتەکراوت نیە. + هیچ بارێکی خشتەکراوت نیە. هیچ ڕەشنووسێکت نییە. هەڵە لە گەڕان بەدوای بابەت %s دەستکاریکردن @@ -271,7 +271,7 @@ ئەژمێرەکان هەژمارەلە ڕاژەیەکی دیکەیە ترە. کۆپیەکی بێ سەروبەر بنێرە بۆ ڕاپۆرتەکە لەوێ؟ ڕاپۆرتەکە دەنێردرێت بۆ بەڕێوەبەری ڕاژەکەت. دەتوانیت ڕوونکردنەوەیەک پێشکەش بکەیت کە بۆچی ئەم ئەژمێرە لە خوارەوە ڕاپۆرت دەکەیت: - سەرکەوتوو نەبوو لە هێنانی بارەکان + سەرکەوتوو نەبوو لە هێنانی بارەکان ڕاپۆرتکردن سەرکەوتوو نەبوو ناردنەوە بۆ %s سەرنجەکانی زیاتر @@ -323,12 +323,12 @@ شوێنکەوتوانی لە لیست نەکراو گشتی - نیشانکراوە - پەسەندکراو - دووبارە بڵاگ کرا - هیچ وەسفێک - ئاگاداری ناوەڕۆک: %s - میدیا: %s + نیشانکراوە + پەسەندکراو + دووبارە بڵاگ کرا + هیچ وەسفێک + ئاگاداری ناوەڕۆک: %s + میدیا: %s بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت @@ -369,8 +369,8 @@ دواتر تۆ پێویستە توسکی دەستپێبکەیتەوە بۆ ئەوەی ئەم گۆڕانکاریانە جێبەجێ بکەیت دەسپێکردنەوەی کاربەرنامە پێویستە - کردنەوە توت - فراوانکردن/نوشتانەوەی هەموو بارەکان + کردنەوە توت + فراوانکردن/نوشتانەوەی هەموو بارەکان ئەنجامدانی گەڕان… تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت سیستەمی بنەڕەت @@ -378,11 +378,11 @@ ڕوونووسکراوە بۆ کلیپ بۆرد نموونەکەت %s هیچ ئیمۆجییەکی ئاسایی نییە دروستکردن - کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت - ناردنی هەڵوەشاوە - ناردنی توتس - هەڵە لە ناردنی توت - (توت) دەنێرم… + کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت + ناردنی هەڵوەشاوە + ناردنی توتس + هەڵە لە ناردنی توت + (توت) دەنێرم… ڕەشنووس پاشەکەوت بکەیت؟ داوات لێدەکات کە بە دەستی شوێنکەوتوانی پەسەند بکە داخستنی ئەژمێر @@ -434,10 +434,10 @@ لە %dd لە %dy بەدواداچوونەوەی داواکراو - ڤیدیۆ - وێنەکان - هاوبەشکردنی لینک بۆ توت - هاوبەشکردنی ناوەڕۆکی دووت + ڤیدیۆ + وێنەکان + هاوبەشکردنی لینک بۆ توت + هاوبەشکردنی ناوەڕۆکی دووت پرۆفایلی تاسکی ڕاپۆرتەکانی هەڵەکان و داواکاریەکانی تایبەتمەندی: \nhttps://github.com/tuskyapp/Tusky/issues @@ -469,12 +469,12 @@ شوێنکەوتوانی نوێ ئاگانامەکان دەربارەی ئاماژە نوێیەکان ئاماژە نوێیەکان - گەورەترین - گەورە - مامناوەندی - بچووک - بچووکترین - قەبارەی دەقی بار + گەورەترین + گەورە + مامناوەندی + بچووک + بچووکترین + قەبارەی دەقی بار شوێنکەوتوانی تەنها لە لیست نەکراو گشتی diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2c15e98f..7840aa9c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,9 +26,9 @@ Přímé zprávy Panely Toot - Tooty - S odpověďmi - Připnuté + Tooty + S odpověďmi + Připnuté Sledovaní Sledující Oblíbené @@ -38,15 +38,15 @@ Upravit váš profil Koncepty Licence - \@%s - %s boostnul/a - Citlivý obsah - Média skryta - Klikněte pro zobrazení - Zobrazit více - Zobrazit méně - Rozbalit - Zabalit + \@%s + %s boostnul/a + Citlivý obsah + Média skryta + Klikněte pro zobrazení + Zobrazit více + Zobrazit méně + Rozbalit + Zabalit Tady nic není. Tady nic není. Obnovte přetažnením dolů! %s boostnul/a váš toot @@ -122,14 +122,14 @@ Sdílet jako… Stáhnout média Stahuji média - Sdílet URL tootu na… - Sdílet toot na… + Sdílet URL tootu na… + Sdílet toot na… Sdílet média na… Odesláno! Uživatel odblokován Uživatel odkryt - Odesláno! - Odpověď byla úspěšně odeslána. + Odesláno! + Odpověď byla úspěšně odeslána. Který server? Co se právě děje? Varování o obsahu @@ -156,7 +156,7 @@ Stáhnout Zrušit požadavek o sledování? Přestat sledovat tento účet? - Smazat tento toot? + Smazat tento toot? Veřejný: Poslat na veřejné časové osy Neuvedený: Neposlat na veřejné časové osy Pouze pro sledující: Poslat pouze sledujícím @@ -185,8 +185,8 @@ Používat Vlastní karty Chrome Skrýt tlačítko pro psaní při rolování Jazyk - Filtrování časových os - Panely + Filtrování časových os + Panely Zobrazi boosty Zobrazit odpovědi Stahovat náhledy médií @@ -202,12 +202,12 @@ Veřejné Neuvedené Pouze pro sledující - Velikost textu příspěvků - Nejmenší - Malý - Střední - Velký - Největší + Velikost textu příspěvků + Nejmenší + Malý + Střední + Velký + Největší Nové zmínky Oznámení o nových zmínkách Noví sledující @@ -242,10 +242,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues
Profil aplikace Tusky - Sdílet obsah tootu - Sdílet odkaz k tootu - Obrázky - Video + Sdílet obsah tootu + Sdílet odkaz k tootu + Obrázky + Video Vyžádáno sledování za %d let @@ -295,11 +295,11 @@ Uzamknout účet Vyžaduje, abyste ručně schvaloval/a sledující Uložit koncept? - Odesílám toot… - Chyba při odesílání tootu - Odesílám tooty - Odesílání bylo zrušeno - Kopie vašeho tootu byla uložena do vašich konceptů + Odesílám toot… + Chyba při odesílání tootu + Odesílám tooty + Odesílání bylo zrušeno + Kopie vašeho tootu byla uložena do vašich konceptů Napsat Vaše instance %s nemá žádná vlastní emoji Zkopírováno do schránky @@ -307,8 +307,8 @@ Výchozí nastavení systému Musíte si nejprve stáhnout tyto sady emoji Provádím prohledávání… - Rozbalit/zabalit všechny příspěvky - Otevřít toot + Rozbalit/zabalit všechny příspěvky + Otevřít toot Je vyžadován restart aplikace Pro použití těchto změn musíte restartovat aplikaci Tusky Později @@ -351,15 +351,15 @@ bylo dosaženo maxima %1$d panelů - Média %s + Média %s - Varování o obsahu: %s + Varování o obsahu: %s - Žádný popis + Žádný popis - Boostnutý + Boostnutý - Oblíbený + Oblíbený Veřejný Neuvedený @@ -376,7 +376,7 @@ Zobrazovat indikátor pro roboty Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení\? Smazat a přepsat - Smazat a přepsat tento toot\? + Smazat a přepsat tento toot\? %1$s • %2$s %s hlas @@ -423,7 +423,7 @@ Dodatečné komentáře Přeposlat na %s Nahlášení selhalo - Stahování tootů neuspělo + Stahování tootů neuspělo Nahlášení bude zasláno moderátorovi vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete: Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii\? Zobrazit filtr oznámení @@ -439,11 +439,11 @@ Lze zvolit více možností Možnost %d Upravit - Plánované tooty + Plánované tooty Upravit Přidat anketu - Plánované tooty - Naplánovat toot + Plánované tooty + Naplánovat toot Obnovit Vždy rozbalovat tooty označené varováními o obsahu Celé slovo @@ -464,12 +464,12 @@ Audio soubory musí být menší než 40MB. Ukazovat náhledy k odkazům Mastodon neumožňuje pracovat s intervalem menším než 5 minut. - Zatím zde nemáte žádné naplánované statusy. + Zatím zde nemáte žádné naplánované statusy. Zatím zde nejsou žádné koncepty. Možnost přetahování prstem pro přechod mezi kartami Seznam Přidat hashtag - Uloženo do Záložek + Uloženo do Záložek Vybrat seznam Umístění hlavní navigační lišty Hashtagy diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index f83df624..ca22d127 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -23,8 +23,8 @@ Lleol Ffedereiddwyd Tŵtio - Negeseuon - Gydag ymatebion + Negeseuon + Gydag ymatebion Dilyniadau Dilynwyr Ffefrynnau @@ -34,14 +34,14 @@ Golygu\'ch Proffil Drafftiau Trwyddedau - %s wedi\'u hybu - Cynnwys sensitif - Cyfryngau cudd - Cliciwch i weld - Dangos Mwy - Dangos Llai - Chwyddo - Lleihau + %s wedi\'u hybu + Cynnwys sensitif + Cyfryngau cudd + Cliciwch i weld + Dangos Mwy + Dangos Llai + Chwyddo + Lleihau Dim byd yma. Tynnwch lawr i adnewyddu! %s wedi hybu\'ch tŵt %s wedi nodi\'ch tŵt yn ffefryn @@ -98,14 +98,14 @@ Bysellfwrdd emoji Lawrlwytho %1$s Copïo\'r ddolen - Rhannu URL Tŵt i… - Rhannu Tŵt i… + Rhannu URL Tŵt i… + Rhannu Tŵt i… Rhannu cyfryngau i… Anfonwyd! Dad-flociwyd y defnyddiwr Dad-fudwyd y defnyddiwr - Anfonwyd! - Anfonwyd yr ateb. + Anfonwyd! + Anfonwyd yr ateb. Pa achos? Beth sy\'n digwydd? Rhybudd cynnwys @@ -132,7 +132,7 @@ Lawrlwytho Tynnu\'r cais i ddilyn yn ôl? Dad-ddilyn y cyfrif hwn? - Dileu\'r tŵt hwn? + Dileu\'r tŵt hwn? Cyhoeddus: Postio i ffrydiau cyhoeddus Heb restru: Peidio â dangos ar ffrydiau cyhoeddus Dilynwyr yn Unig: Postio i ddilynwyr yn unig @@ -157,8 +157,8 @@ Porwr Defnyddio Tabiau Personol Chrome Cuddio\'r botwm creu wrth sgrolio - Hidlo ffrwd - Tabiau + Hidlo ffrwd + Tabiau Dangos hybiadau Dangos atebion Dangos rhagolwg o gyfryngau @@ -172,12 +172,12 @@ Cyhoeddus Heb ei restru Dilynwyr yn unig - Maint testun statws - Lleiaf - Bach - Canolig - Mawr - Mwyaf + Maint testun statws + Lleiaf + Bach + Canolig + Mawr + Mwyaf Yn sôn amdanoch o\'r newydd Hysbysiadau sôn amdanoch o\'r newydd Dilynwyr Newydd @@ -211,10 +211,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Proffil Tusky - Rhannu cynnwys tŵt - Rhannu dolen i\'r tŵt - Delweddau - Fideo + Rhannu cynnwys tŵt + Rhannu dolen i\'r tŵt + Delweddau + Fideo Gofyn i ddilyn %dy @@ -244,11 +244,11 @@ Cloi cyfrif Angen cymeradwyo dilynwyr eich hun Cadw drafft? - Yn anfon Tŵt… - Gwall wrth anfon Tŵt - Yn anfon Tŵtiau - Canslo anfon - Cadwyd copi o\'r tŵt i\'ch drafftiau + Yn anfon Tŵt… + Gwall wrth anfon Tŵt + Yn anfon Tŵtiau + Canslo anfon + Cadwyd copi o\'r tŵt i\'ch drafftiau Creu Nid oes gan eich achos %s emoji bersonol Copïwyd i\'r clipfwrdd @@ -256,8 +256,8 @@ Rhagosodiad system Bydd angen i chi lawrlwytho\'r setiau emoji hyn yn gyntaf Wrthi\'n chwilio… - Chwyddo/lleihau pob statws - Agor tŵt + Chwyddo/lleihau pob statws + Agor tŵt Angen ailddechrau\'r app Bydd angen ailddechrau Tusky i roi\'r newidiadau ar waith Nes ymlaen @@ -275,11 +275,11 @@ ychwanegu data Cynnwys Defnyddio amser absoliwt - \@%s + \@%s Digwyddodd gwall rhwydwaith! Gwiriwch eich cysylltiad a cheisiwch eto! Negeseuon Uniongyrchol Tabiau - Wedi\'i binio + Wedi\'i binio Parthau cudd Dim byd yma. Dileu hwb diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7515ccd9..fef59401 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -26,9 +26,9 @@ Direktnachrichten Tabs Beitrag - Beiträge - mit Antworten - Angeheftet + Beiträge + mit Antworten + Angeheftet Folgt Folgende Favoriten @@ -38,15 +38,15 @@ Dein Profil bearbeiten Entwürfe Lizenzen - \@%s - %s teilte - Heikle Inhalte - Medien versteckt - Zum Anzeigen tippen - Zeige mehr - Zeige weniger - Mehr - Weniger + \@%s + %s teilte + Heikle Inhalte + Medien versteckt + Zum Anzeigen tippen + Zeige mehr + Zeige weniger + Mehr + Weniger Hier ist nichts. Noch keine Beiträge hier! Ziehe nach unten um zu aktualisieren! %s teilte deinen Beitrag @@ -119,14 +119,14 @@ Link kopieren Öffne als %s Teilen als … - Beitragslink teilen… - Beitragsinhalt teilen… + Beitragslink teilen… + Beitragsinhalt teilen… Mediendatei teilen… Gesendet! entblockt Stummschaltung aufgehoben - Gesendet! - Antwort erfolgreich gesendet. + Gesendet! + Antwort erfolgreich gesendet. Welche Instanz? Was gibt\'s Neues? Inhaltswarnung @@ -153,7 +153,7 @@ Herunterladen Folgeanfrage zurückziehen? Willst du diesem Profil wirklich nicht mehr folgen? - Diesen Beitrag löschen? + Diesen Beitrag löschen? Öffentlich: Für alle sichtbar Ungelistet: Nicht in der öffentlichen Timeline sichtbar Nur Folgende: Nur für Folgende sichtbar @@ -182,8 +182,8 @@ Links in der App (Browser Custom Tabs) öffnen Verstecke Button bei Bildlauf Sprache - Timeline-Filter - Tabs + Timeline-Filter + Tabs Geteilte Beiträge anzeigen Zeige Antworten Medienvorschauen herunterladen @@ -199,20 +199,20 @@ Öffentlich Nicht gelistet Nur Folgende - Schriftgröße - Kleiner - Klein - Normal - Groß - Größer + Schriftgröße + Kleiner + Klein + Normal + Groß + Größer Neue Erwähnungen Benachrichtigungen über neue Erwähnungen Neue Folgende Benachrichtigunen über neue Folgende Geteilte Beiträge - Benachrichtigungen wenn deine Beiträge geteilt werden + Benachrichtigungen, wenn deine Beiträge geteilt werden Favorisierte Beiträge - Benachrichtigungen wenn deine Beiträge favorisiert werden + Benachrichtigungen, wenn deine Beiträge favorisiert werden %s hat dich erwähnt %1$s, %2$s, %3$s und %4$d andere %1$s, %2$s, und %3$s @@ -236,10 +236,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Tuskys Profil - Inhalt teilen - Link teilen - Bilder - Video + Inhalt teilen + Link teilen + Bilder + Video Folgeanfrage gesendet Folgt dir @@ -272,11 +272,11 @@ Gesperrtes Profil Wer dir folgen möchte, muss um deine Erlaubnis bitten Entwurf speichern? - Beitrag senden… - Fehler beim Senden - Beiträge senden - Senden abgebrochen - Eine Kopie des Beitrags wurde in deine Entwürfe gespeichert + Beitrag senden… + Fehler beim Senden + Beiträge senden + Senden abgebrochen + Eine Kopie des Beitrags wurde in deine Entwürfe gespeichert Beitrag erstellen Deine Instanz %s hat keine Emojis definiert In die Zwischenablage kopiert @@ -284,8 +284,8 @@ System-Standard Du musst diese Emoji-Sets zunächst herunterladen Nachschlagen… - Alle Beiträge aus-/einklappen - Beitrag öffnen + Alle Beiträge aus-/einklappen + Beitrag öffnen App-Neustart erforderlich Du musst Tusky neustarten um die Änderungen anzuwenden Später @@ -319,8 +319,8 @@ Maximum von %1$d Tab erreicht Maximum von %1$d Tabs erreicht - Keine Beschreibung - Favorisiert + Keine Beschreibung + Favorisiert Öffentlich Folgende Direkt @@ -341,12 +341,12 @@ <b>%1$s</b> Favoriten Geteilt von - Medien: %s - Inhaltswarnung: %s - Geteilt + Medien: %s + Inhaltswarnung: %s + Geteilt Ungelistet Löschen und neu erstellen - Bist du dir sicher, dass du diesen Beitrag löschen und neu erstellen möchtest\? + Bist du dir sicher, dass du diesen Beitrag löschen und neu erstellen möchtest\? Umfragen beendet sind Umfragen Benachrichtigungen über beendete Umfragen @@ -416,28 +416,28 @@ Beiträge mit Inhaltswarnungen immer ausklappen Umfrage mit den Möglichkeiten: %1$s, %2$s, %3$s, %4$s; %5$s Aktionen für Bild %s - Fehler beim Holen der Beiträge + Fehler beim Holen der Beiträge Konten Fehler beim Suchen Auswahlmöglichkeit hinzufügen Mehrere Möglichkeiten Möglichkeit %d - Geplante Beiträge + Geplante Beiträge Editieren - Geplante Beiträge - Plane Beitrag + Geplante Beiträge + Plane Beitrag Zurücksetzen Audiodateien müssen kleiner als 40 MB sein. Lesezeichen Lesezeichen Lesezeichen Angetrieben durch Tusky - Als Lesezeichen gespeichert + Als Lesezeichen gespeichert Liste auswählen Liste Fehler beim Nachschlagen von Post %s Du hast keine Entwürfe. - Du hast keine geplanten Beiträge. + Du hast keine geplanten Beiträge. Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen. Benachrichtigungen über neue Folgeanfragen Neue Folgeanfragen @@ -478,9 +478,9 @@ Titel der Hauptnavigation verstecken Im Moment gibt es keine Ankündigungen. Ankündigungen - Der Beitrag auf den du antworten willst wurde gelöscht + Der Beitrag auf den du antworten willst wurde gelöscht Entwurf gelöscht - Dieser Beitrag konnte nicht gesendet werden! + Dieser Beitrag konnte nicht gesendet werden! Willst du die Liste %s wirklich löschen\? Du kannst nicht mehr als %1$d Anhang hochladen. @@ -489,12 +489,12 @@ Wohlbefinden Dauer Für immer - Anhänge - Audio + Anhänge + Audio Benachrichtigungen, wenn jemand, den ich abonniert habe, etwas Neues veröffentlicht Neue Beiträge GIF-Emojis animieren - Jemand, den ich abonniert habe, etwas Neues veröffentlicht + Jemand, den ich abonniert habe, hat etwas Neues veröffentlicht %s hat gerade etwas gepostet %d Min. Benachrichtigungen überprüfen @@ -521,4 +521,11 @@ Bestätigungsdialog vor dem Favorisieren eines Beitrags Diese Unterhaltung wirklich löschen\? Unterhaltung löschen + 30 Tage + 60 Tage + 90 Tage + 365 Tage + 14 Tage + 180 Tage + Beitrag erstellen \ No newline at end of file diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 06bc38e1..de167b85 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -1,23 +1,59 @@ The account is from another server. Send an anonymised copy of the report there as well\? - Favourited + Favourited Favourited by <b>%1$s</b> Favourite <b>%1$s</b> Favourites - Notifications when your toots get marked as favourite + Notifications when your posts get marked as favourite Favourites my posts are favourited Show favourites Favourites Remove favourite Favourite - %s favourited your toot + %s favourited your post Favourites Authorisation was denied. An unidentified authorisation error occurred. Log out Show colourful gradients for hidden media + Edit your profile + Permission to read media is required. + Images and videos cannot both be attached to the same post. + A network error occurred! Please check your connection and try again! + Failed authenticating with that instance. + Couldn\'t find a web browser to use. + Failed getting a login token. + Notifications + Posts + Follows + Hidden domains + That type of file cannot be uploaded. + Federated + The upload failed. + Bookmarks + That file could not be opened. + Direct Messages + Followers + The post is too long! + An error occurred. + Local + Blocked users + Video files must be less than 40MB. + Tabs + Audio files must be less than 40MB. + Follow Requests + Home + Muted users + Thread + The file must be less than 8MB. + Error sending post. + This cannot be empty. + Permission to store media is required. + Invalid domain entered + With replies + Pinned \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 4f99bbf6..5b47ccd0 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -26,9 +26,9 @@ Rektaj mesaĝoj Langetoj Mesaĝo - Mesaĝoj - Kun respondoj - Alpinglitaj + Mesaĝoj + Kun respondoj + Alpinglitaj Sekvatoj Sekvantoj Stelumoj @@ -38,15 +38,15 @@ Redakti vian profilon Malnetoj Permesiloj - \@%s - %s diskonigis - Tikla enhavo - Kaŝitaj aŭdovidaĵoj - Alklaki por vidi - Montri pli - Montri malpli - Pligrandigi - Malgrandigi + \@%s + %s diskonigis + Tikla enhavo + Kaŝitaj aŭdovidaĵoj + Alklaki por vidi + Montri pli + Montri malpli + Pligrandigi + Malgrandigi Nenio ĉi tie. Nenio ĉi tie. Tiru malsupren por aktualigi! %s diskonigis vian mesaĝon @@ -122,14 +122,14 @@ Konigi kiel … Elŝuti aŭdovidaĵon Elŝutante aŭdovidaĵo - Konigi URL de mesaĝo al… - Konigi mesaĝon al… + Konigi URL de mesaĝo al… + Konigi mesaĝon al… Konigi aŭdovidaĵon al… Sendita! Malblokita uzanto Malsilentigita uzanto - Sendita! - Respondi sukcese sendita. + Sendita! + Respondi sukcese sendita. Kiu nodo? Kio okazas? Enhava averto @@ -152,7 +152,7 @@ Elŝuti Nuligi peton de sekvado? Ne plu sekvi? - Forigi ĉi tiun mesaĝon? + Forigi ĉi tiun mesaĝon? Publika: afiŝi en publikaj tempolinioj Nelistigita: Ne afiŝi en publikaj tempolinioj Nur por sekvantoj: Afiŝi nur al sekvantoj @@ -181,8 +181,8 @@ Uzi la integritan retumilon Kâsi butonon de verko dum rulumado Lingvo - Filtrado de tempolinioj - Langetoj + Filtrado de tempolinioj + Langetoj Montri diskonigojn Montri la respondojn Elŝuti antaŭvidojn de aŭdovidaĵoj @@ -198,12 +198,12 @@ Publika Nelistigita Nur por sekvantoj - Grando de teksto de mesaĝoj - La plej malgranda - Malgranda - Meza - Granda - La plej granda + Grando de teksto de mesaĝoj + La plej malgranda + Malgranda + Meza + Granda + La plej granda Novaj mencioj Sciigoj pri novajn menciojn Novaj sekvantoj @@ -239,10 +239,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Profilo de Tusky - Konigi enhavon de la mesaĝo - Konigi ligilon al mesaĝo - Bildoj - Video + Konigi enhavon de la mesaĝo + Konigi ligilon al mesaĝo + Bildoj + Video Sekvado petita en %dj @@ -292,11 +292,11 @@ Ŝlosi konton Vi devas permane rajtigi sekvantojn Konservi malneton? - Sendante la mesaĝo… - Eraro dum sendo de la mesaĝo - Sendante la mesaĝoj - Sendo nuligita - Kopio de la mesaĝo estis konservita en viaj malnetoj + Sendante la mesaĝo… + Eraro dum sendo de la mesaĝo + Sendante la mesaĝoj + Sendo nuligita + Kopio de la mesaĝo estis konservita en viaj malnetoj Verki Via nodo %s ne havas proprajn emoĝiojn Kopiita en tondujo @@ -304,8 +304,8 @@ Sistema valoro Vi unue devos elŝuti ĉi tiujn emoĝiarojn Serĉante… - Pligrandigi/malgrandigi ĉiujn mesaĝojn - Malfermi mesaĝon + Pligrandigi/malgrandigi ĉiujn mesaĝojn + Malfermi mesaĝon Restartigo necesas Vi devos restartigi Tusky por apliki ĉi tiujn ŝanĝojn Poste @@ -347,15 +347,15 @@ maksimumo da %1$d langeto atingita maksimumo da %1$d langetoj atingita - Aŭdovidaĵo: %s + Aŭdovidaĵo: %s - Enhava averto: %s + Enhava averto: %s - Neniu priskribo + Neniu priskribo - Diskonigita + Diskonigita - Stelumita + Stelumita Publika @@ -365,7 +365,7 @@ Rekta Nomo de la listo Forigi kaj reskribi - Ĉu forigi kaj reskribi ĉi-tiun mesaĝon\? + Ĉu forigi kaj reskribi ĉi-tiun mesaĝon\? enketoj finiĝis Montri indikilon por robotoj Moviĝi GIF profilbildojn @@ -404,7 +404,7 @@ Pliaj komentoj Plusendi al %s Signalo malsukcesis - Venigo de statusoj malsukcesis + Venigo de statusoj malsukcesis La signalo estos sendita al la kontrolantoj de via servilo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube: La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien\? Montri filtrilon de Sciigoj @@ -427,21 +427,21 @@ Elekton %d Redakti Legosignoj - Planitaj mesaĝoj + Planitaj mesaĝoj Aldoni al la legosignoj Redakti Legosignoj - Planitaj mesaĝoj - Plani mesaĝon + Planitaj mesaĝoj + Plani mesaĝon Restarigi Funkciigita de Tusky - Aldonita al la legosignoj + Aldonita al la legosignoj Elekti la liston Listo Eraro dum elserĉo de la mesaĝo %s Aŭdia dosiero devas esti malpli ol 40MB. Vi ne havas iun ajn malneton. - Vi ne havas iun ajn planitan mesaĝon. + Vi ne havas iun ajn planitan mesaĝon. Petoj de sekvado Kradvortoj @@ -490,10 +490,10 @@ Estas neniu anonco. Ebligi ŝovumadon por ŝanĝi inter la langetoj Mastodon havas minimuman intervalon de planado de 5 minutoj. - Kunsendaĵoj + Kunsendaĵoj iu kiun mi sekvas afiŝis novan mesaĝon Ĉu vi vere volas forigi la liston %s\? - Aŭdio + Aŭdio Aboni Malneto forigita @@ -511,7 +511,7 @@ Forigi konversacion %s ĵus afiŝis Sciigoj kiam iu kiun vi sekvas afiŝis novan mesaĝon - Sendo de ĉi-tiu mesaĝo malsukcesis! + Sendo de ĉi-tiu mesaĝo malsukcesis! Kaŝi kvantecajn statistikaĵojn sur la mesaĝoj Demandi konfirmon antaŭ ol stelumi Bonstato @@ -525,5 +525,5 @@ \n Puŝosciigoj ne estos influitaj, sed vi povas kontroli viajn sciigojn preferojn permane. Kontroli la sciigojn Limigi sciigojn pri tempolinio - La mesaĝo al kiu ĉi tiu malneto respondas estis forigita + La mesaĝo al kiu ĉi tiu malneto respondas estis forigita \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5bc4dd3b..43b072a5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -26,9 +26,9 @@ Mensajes Directos Pestañas Publicación - Estados - Con respuestas - Fijado + Estados + Con respuestas + Fijado Siguiendo Seguidores Favoritos @@ -38,15 +38,15 @@ Editar tu perfil Borradores Licencias - \@%s - %s compartió - Contenido sensible - Material oculto - Pulsa para ver - Mostrar más - Mostrar menos - Expandir - Ocultar + \@%s + %s compartió + Contenido sensible + Material oculto + Pulsa para ver + Mostrar más + Mostrar menos + Expandir + Ocultar Nada aquí. Nada por aquí. ¡Arrastra hacia abajo para recargar! %s impulsó tu toot @@ -108,14 +108,14 @@ Copiar el enlace Abrir como %s Compartir como… - Compartir URL… - Compartir contenido… + Compartir URL… + Compartir contenido… Compartir medios a… ¡Enviado! El usuario ya no está bloqueado El usuario ya no está silenciado - ¡Enviado! - Respuesta enviada correctamente. + ¡Enviado! + Respuesta enviada correctamente. ¿Qué instancia\? ¿En qué estás pensando? Aviso de contenido @@ -142,7 +142,7 @@ Descargar ¿Cancelar petición de amistad? ¿Dejar de seguir esta cuenta? - ¿Eliminar este toot\? + ¿Eliminar este toot\? Público: Mostrar en historias públicas Oculto: No mostrar en historias públicas Privado: Sólo visible para seguidores @@ -168,8 +168,8 @@ Navegador Usar pestañas de Chrome Ocultar botón de redacción al bajar - Filtros de cronología - Pestañas + Filtros de cronología + Pestañas Mostrar impulsos Mostrar respuestas Previsualizar multimedia @@ -185,12 +185,12 @@ Público Oculto Privado - Tamaño del texto - Diminuto - Pequeño - Medio - Grande - Enorme + Tamaño del texto + Diminuto + Pequeño + Medio + Grande + Enorme Nuevas menciones Notificaciones de nuevas menciones Nuevos seguidores @@ -226,10 +226,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Perfil de Tusky - Compartir contenido - Compartir enlace - Imágenes - Video + Compartir contenido + Compartir enlace + Imágenes + Video Solicitud enviada en %dy @@ -262,11 +262,11 @@ Proteger cuenta Tendrá que admitir los seguidores manualmente ¿Guardar borrador? - Enviando estado… - Error al enviar el estado - Enviando estado - Envío cancelado - Una copia del estado se ha guardado en borradores + Enviando estado… + Error al enviar el estado + Enviando estado + Envío cancelado + Una copia del estado se ha guardado en borradores Redactar Su instancia %s no ofrece emojis personalizados Copiado al portapapeles @@ -274,8 +274,8 @@ Sistema Tendrás que descargarlos primero Buscando… - Expandir/ocultar todos los estados - Abrir + Expandir/ocultar todos los estados + Abrir Reinicio requerido Tendrás que reiniciar la aplicación para aplicar estos cambios Más tarde @@ -332,7 +332,7 @@ Renombrar la lista Añadir cuenta a la lista Eliminar cuenta de la lista - 1Favoritos + 1Favoritos Seguidores Aplicar Mostrar indicador de bots @@ -366,7 +366,7 @@ Enlaces Abrir contenido #%d Descargando contenido - ¿Eliminar y devolver a borradores este toot\? + ¿Eliminar y devolver a borradores este toot\? encuestas han terminado Notificaciones sobre encuestas que han terminado Cronologías públicas @@ -381,10 +381,10 @@ Eliminar la lista Editar la lista Buscar personas que sigues - Contenido: %s - Aviso de contenido: %s - Sin descripción - Compartido + Contenido: %s + Aviso de contenido: %s + Sin descripción + Compartido Público Sin listar Directo @@ -419,7 +419,7 @@ Comentarios adicionales Reenviar a %s Reporte fallido - Fallo al obtener estados + Fallo al obtener estados El reporte será enviado a un moderador de tu servidor. Puedes añadir una explicación de por qué estás reportando esta cuenta a continuación: La cuenta es de otro servidor. ¿Enviar una copia anónima del reporte\? Mostrar filtro de notificaciones @@ -439,22 +439,22 @@ Opciones múltiples Opción %d Editar - Estados programados + Estados programados Editar - Estados programados - Programar estado + Estados programados + Programar estado Reiniciar Error al buscar el post %s Potenciado por Tusky Marcadores Marcador Marcadores - Marcado como favorito + Marcado como favorito Seleccionar lista Lista Los ficheros de audio deben ser menores de 40MB. No tienes ningún borrador. - No tienes ningún estado programado. + No tienes ningún estado programado. Mastodon tiene un intervalo de programación mínimo de 5 minutos. Solicitudes Bloquear @%s\? @@ -506,15 +506,15 @@ \n- Estadísticas de seguidores e toots en perfiles \n \nLas notificaciones Push no serán afectadas, pero puedes revisar manualmente tus preferencias. - El toot al que redactaste una respuesta ha sido eliminado + El toot al que redactaste una respuesta ha sido eliminado Borrador eliminado Error al cargar la información de respuesta - ¡Este toot no se pudo enviar! + ¡Este toot no se pudo enviar! ¿Realmente quieres eliminar la lista %s\? Indefinido Duración - Adjuntos - Audio + Adjuntos + Audio Limitar cronología de notificaciones Quitar marcador Aunque su cuenta no está bloqueada, el personal de %1$s pensó que podría querer revisar las solicitudes de seguimiento de estas cuentas manualmente. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 52722060..791575b7 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -23,8 +23,8 @@ Lokala Federatua Tuta - Tutak - Erantzunekin + Tutak + Erantzunekin Jarraitzen Jarraitzaileak Gogokoak @@ -34,14 +34,14 @@ Profila editatu Zirriborroak Lizentziak - %s(e)k bultzatu du - Kontuz edukiarekin - Ezkutuko multimedia - Sakatu ikusteko - Gehiago erakutsi - Gutxiago erakutsi - Zabaldu - Bildu + %s(e)k bultzatu du + Kontuz edukiarekin + Ezkutuko multimedia + Sakatu ikusteko + Gehiago erakutsi + Gutxiago erakutsi + Zabaldu + Bildu Edukirik ez. Arrastatu behera birkargatzeko! %s(e)k zure tuta bultzatu du %s(e)k zure tuta gogoko du @@ -99,14 +99,14 @@ Emoji teklatua %1$s jaisten Lotura kopiatu - Tutaren URLa partekatu… - Tuta partekatu… + Tutaren URLa partekatu… + Tuta partekatu… Partekatu media hona… Bidalia! Erabiltzailea desblokeatuta Erabiltzailea isilgabetuta - Bidalia! - Erantzuna ongi bidali da. + Bidalia! + Erantzuna ongi bidali da. Zein instantzia\? Zer duzu buruan? Edukiaren abisua @@ -131,7 +131,7 @@ Jaitsi Jarraipen-eskakizunari uko egin\? Kontu hau jarraitzeari utzi\? - Tuta ezabatu\? + Tuta ezabatu\? Publikoa: Istorio publikoetan erakutsi Ezkutukoa: Ez erakutsi istorio publikoetan Pribatua: Jarraitzaileentzat soilik ikusgai @@ -157,8 +157,8 @@ Nabigatzailea Chromeko fitxak erabili Tut egiteko botoia ezkutatu beherantz joaterakoan - Denbora-lerro filtroak - Fitxak + Denbora-lerro filtroak + Fitxak Bultzadak erakutsi Erakutsi erantzunak Jaitsi mediaren aurreikuspenak @@ -174,12 +174,12 @@ Publiko Zerrendagabetuta Jarraitzaileak soilik - Status testuaren tamaina - Oso txikia - Txikia - Erdikoa - Handia - Handiena + Status testuaren tamaina + Oso txikia + Txikia + Erdikoa + Handia + Handiena Aipamen berriak Aipamen berrien jakinarazpenak Jarritzaile berriak @@ -213,10 +213,10 @@ Akatsen berri-emateak eta hobekuntza-eskariak: \n https://git.chinwag.org/chinwag/chinwag-android/issues Tuskyren profila - Partekatu tutaren edukia - Partekatu tutaren lotura - Irudiak - Bideoak + Partekatu tutaren edukia + Partekatu tutaren lotura + Irudiak + Bideoak Eskaera bidalita %du-an @@ -246,11 +246,11 @@ Kontua babestu Jarraitzaileak eskuz onartu beharko dituzu Zirriborroa gorde? - Tuta bidaltzen… - Errorea tuta bidaltzerakoan - Tuta bidaltzen - Bidalketa ezeztatua - Tutaren kopia zirriborroetan sartu da + Tuta bidaltzen… + Errorea tuta bidaltzerakoan + Tuta bidaltzen + Bidalketa ezeztatua + Tutaren kopia zirriborroetan sartu da Idatzi %s instantziak ez ditu emoji pertsonalizatuak eskaintzen Arbelean kopiatua @@ -258,8 +258,8 @@ Sistema Lehenago jaitsi beharko dituzu Bilatzen… - Tut guztiak ezkutatu/zabaldu - Ireki + Tut guztiak ezkutatu/zabaldu + Ireki Berrabiaraztea beharrezkoa da Aplikazioa berrabiarazi beharko duzu aldaketa ezartzeko Beranduago @@ -285,10 +285,10 @@ Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez! Mezu Zuzenak Fitxak - Lotuta + Lotuta Ezkutuko domeinuak - Programatutako tutak - \@%s + Programatutako tutak + \@%s Kilkerrak besterik ez hemen. Bultzada kendu Gogokoa kendu @@ -297,8 +297,8 @@ Ezkutuko domeinuak Galdeketa gehitu Mututu %s - Programatutako tutak - Tuta programatu + Programatutako tutak + Tuta programatu Berrezarri Kategoria gehitu Estekak @@ -313,7 +313,7 @@ Media jaisten Media jaisten %s ez dago ezkutatua - Tut hau ezabatu eta zirriborro berria egin\? + Tut hau ezabatu eta zirriborro berria egin\? Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira. Domeinu osoa ezkutatu Galdeketak bukatu dira @@ -370,11 +370,11 @@ gehienezko %1$d fitxa iritsita gehienezko %1$d fitxa iritsita - Media: %s - Edukiaren abisua: %s - Deskribapenik ez - Birblogeatuta - Gogotuta + Media: %s + Edukiaren abisua: %s + Deskribapenik ez + Birblogeatuta + Gogotuta Publiko Zerrendagabetuta Jarraitzaileak @@ -418,7 +418,7 @@ Iruzkin gehigarriak %s(r)i birbidali Txostena huts egin du - Egoeren eskuratzea huts egin du + Egoeren eskuratzea huts egin du Txostena zure zerbitzariaren moderatzaileari bidaliko zaio. Jarraian, kontu honen zergatia salatzen duzun azalpena eman dezakezu: Kontua beste zerbitzari batekoa da. Bidali txostenaren kopia anonimatua hara ere\? Kontuak @@ -442,12 +442,12 @@ Laster-markak Ireki bultzadaren egilea Denbora lerro publikoak - Laster-markatuta + Laster-markatuta Audioak 40MB baino gutxiago izan behar ditu. Aukeratu zerrenda Zerrenda Ez duzu zirriborrorik. - Ez duzu tut programaturik. + Ez duzu tut programaturik. Mastodonek gutxienez 5 minutuko programazio-tartea du. Eskakizunak Jarraitzeko eskaereri buruzko jakinarazpenak @@ -480,16 +480,16 @@ Erakutsi baieztapen elkarrizketa-koadroa gogokoenetara gehitu aurretik Zure kontua blokeatuta ez badago ere, %1$s-ko langileek kontu hauetako eskaerak eskuz berrikusi nahi dituzula pentsatu dute. harpidedun naizen norbaitek tut berria argitaratu du - Eranskinak + Eranskinak Ziur %s zerrenda ezabatu nahi duzula\? - Audioa + Audioa Harpidetu Elkarrizketa ezabatu nahi duzu\? Animatu emoji pertsonalizatuak Erantzunaren informazioa ezin izan da kargatu Profiletan estatistika kuantitatiboak ezkutatu Zirriborroa ezabatu da - Erantzuna idatzi zenuen tuta ezabatu da + Erantzuna idatzi zenuen tuta ezabatu da Ongizatea Ezin duzu multimedia eranskin %1$d baino gehiago kargatu. @@ -511,7 +511,7 @@ Iragarpenak Jakinarazpenak harpidetuta zauden norbaitek tut berria argitaratu duenean Kontu honi buruzko zure ohar pribatua - Tut honek ezin izan du bidali! + Tut honek ezin izan du bidali! Zure ongizate mentalean eragina izan dezaketen zenbait informazio ezkutatuta egongo dira. Honek honako hauek ditu: \n \n - Gogokoak, bultzadak eta jarraitzaileen jakinarazpenak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 538f5afe..880e0e4a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -23,8 +23,8 @@ محلّی همگانی بوق - فرسته - با پاسخ‌ + فرسته + با پاسخ‌ دنبال شونده پی‌گیر برگزیده‌ها @@ -34,14 +34,14 @@ ویرایش نمایه‌تان پیش‌نویس‌ها پروانه‌ها - %s تقویت کرد - محتوای حسّاس - رسانهٔ نهفته - کلیک برای نمایش - نمایش بیش‌تر - نمایش کم‌تر - گسترش - بستن + %s تقویت کرد + محتوای حسّاس + رسانهٔ نهفته + کلیک برای نمایش + نمایش بیش‌تر + نمایش کم‌تر + گسترش + بستن این‌جا هیچ‌چیز نیست. برای تازه‌سازی، به پایین بکشید! %s بوقتان را تقویت کرد %s بوقتان را برگزید @@ -98,14 +98,14 @@ صفحه‌کلید اموجی درحال بارگیری %1$s رونوشت از پیوند - هم‌رسانی نشانی بوق با… - هم‌رسانی بوق با… + هم‌رسانی نشانی بوق با… + هم‌رسانی بوق با… هم‌رسانی رسانه با… فرستاده شد! کاربرنامسدود شد کاربر ناخموش شد - فرستاده شد! - پاسخ با موفّقیت فرستاده شد‌. + فرستاده شد! + پاسخ با موفّقیت فرستاده شد‌. کدام نمونه؟ چه خبر؟ هشدار محتوا @@ -130,7 +130,7 @@ بارگیری درخواست دنبال کردن را لغو می‌کنید؟ ناپیگیری این حساب؟ - حذف این بوق؟ + حذف این بوق؟ عمومی: فرستادن به خط زمانی‌های عمومی فهرست‌نشده: نشان ندادن در خط زمانی‌های عمومی تنها دنبال‌کنندگان:پست فقط به دنبال‌کنندگان @@ -156,8 +156,8 @@ مرورگر استفاده از زبانه‌های سفارشی کروم نهفتن دکمهٔ ایجاد، هنگام پیمایش - فیلتر کردن خط زمانی - زبانه‌ها + فیلتر کردن خط زمانی + زبانه‌ها نمایش تقویت‌ها نمایش پاسخ‌ها بارگیری پیش‌نمایش رسانه @@ -173,12 +173,12 @@ عمومی فهرست‌نشده فقط پی‌گیران - اندازهٔ متن وضعیت - کوچک‌ترین - کوچک - متوسط - بزرگ - بزرگ‌ترین + اندازهٔ متن وضعیت + کوچک‌ترین + کوچک + متوسط + بزرگ + بزرگ‌ترین اشاره‌های جدید آگاهی‌ها دربارهٔ اشاره‌های جدید پی‌گیران جدید @@ -209,10 +209,10 @@ گزارش مشکلات و درخواست ویژگی‌ها: \n https://git.chinwag.org/chinwag/chinwag-android/issues نمایهٔ تاسکی - هم‌رسانی محتوای بوق - هم‌رسانی پیوند بوق - تصویرها - ویدیو + هم‌رسانی محتوای بوق + هم‌رسانی پیوند بوق + تصویرها + ویدیو تقاضای پیگیری شد در %d سال @@ -241,11 +241,11 @@ قفل حساب لازم است پی‌گیران را دستی تأیید کنید ذخیرهٔ پیش‌نویس؟ - در حال فرستادن بوق… - خطای فرستادن بوق - در حال فرستادن بوق‌ها - فرستادن لغو شد - رونوشتی از بوق در پیش‌نویس‌هایتان ذخیره شد + در حال فرستادن بوق… + خطای فرستادن بوق + در حال فرستادن بوق‌ها + فرستادن لغو شد + رونوشتی از بوق در پیش‌نویس‌هایتان ذخیره شد ایجاد نمونه‌تان %s هیچ اموجی سفارشی‌ای ندارد در تخته‌گیره رونوشت شد @@ -253,8 +253,8 @@ پیش‌گزیدهٔ سامانه نخست باید این مجموعه‌های اموجی را بارگیری کنید در حال جست‌وجو… - گسترده/جمع کردن تمام وضعیت‌ها - گشودن بوق + گسترده/جمع کردن تمام وضعیت‌ها + گشودن بوق نیاز به آغاز دوبارهٔ کاره برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید بعداً @@ -280,9 +280,9 @@ یک خطای شبکه رخ داد! لطفا اتصال خود را بررسی و دوباره تلاش کنید! پیام‌های مستقیم زبانه‌ها - سنجاق‌شده + سنجاق‌شده دامنه‌های نهفته - \@%s + \@%s این‌جا هیچ‌چیزی نیست. برداشتن تقویت برداشتن برگزیدگی @@ -306,7 +306,7 @@ بارگیری رسانه در حال بارگیری رسانه %s نانهفته - می‌خواهید این بوق را پاک و بازنویسی کنید؟ + می‌خواهید این بوق را پاک و بازنویسی کنید؟ نهفتن تمام دامنه پایان نظرسنجی‌ها پالایه‌ها @@ -360,11 +360,11 @@ رسیده به بیشینهٔ %1$d زبانه رسیده به بیشینهٔ %1$d زبانه - رسانه: %s - هشدار محتوا: %s - بدون هیچ توضیحی - بازبوقیده - برگزیده + رسانه: %s + هشدار محتوا: %s + بدون هیچ توضیحی + بازبوقیده + برگزیده عمومی فهرست‌نشده پی‌گیران @@ -402,7 +402,7 @@ نظرهای اضافی هدایت به %s شکست در گزارش - شکست در واکشی وضعیت‌ها + شکست در واکشی وضعیت‌ها حساب‌ها شکست در جست‌وجو نمایش پالایهٔ آگاهی‌ها @@ -418,10 +418,10 @@ گزینه‌های چندگانه گزینهٔ %d ویرایش - بوق‌های زمان‌بسته + بوق‌های زمان‌بسته ویرایش - بوق‌های زمان‌بسته - بوق زمان‌بسته + بوق‌های زمان‌بسته + بوق زمان‌بسته بازنشانی مطمئنید می‌خواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچ‌یک از خط زمانی‌ها یا در آگاهی‌هایتان نخواهید دید. پی‌گیرانتان از آن دامنه، برداشته خواهند شد. هنگامی که کلیدواژه یا عبارت، فقط حروف‌عددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد @@ -435,11 +435,11 @@ نشانک‌ها نشانک نشانک‌ها - نشان‌شده + نشان‌شده گزینش فهرست فهرست هیچ پیش‌نویسی ندارید. - هیچ وضعیت زمان‌بسته‌ای ندارید. + هیچ وضعیت زمان‌بسته‌ای ندارید. ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد. نمایش گفت‌وگوی تأیید، پیش از تقویت پیش‌نمایش پیوندها در خط‌زمانی‌ها @@ -484,21 +484,21 @@ عدم اشتراک اشتراک پیش‌نویس حذف شد - فرستادن این بوق شکست خورد! + فرستادن این بوق شکست خورد! نهفتن آمار کمی روی نمایه‌ها نهفتن آمار کمی روی فرسته‌ها محدود کردن آگاهی‌های خط‌زمانی بازبینی آگاهی‌ها سلامتی طول - پیوست‌ها - صدا + پیوست‌ها + صدا آگاهی‌ها هنگام انتشار بوقی جدید از کسی که مشترکش هستید بوق‌های جدید اموجی‌های شخصی متحرّک کسی که مشترکش شده‌ام، بوقی جدید منتشر کرد %s چیزی فرستاد - بوقی که پاسخی به آن را پیش‌نویس کردید، برداشته شده + بوقی که پاسخی به آن را پیش‌نویس کردید، برداشته شده شکست در بار کردن اطّلاعات پاسخ برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون: \n diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3638aeaf..cd3ada40 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -4,7 +4,7 @@ Animoi GIF-avatarit Seuraa laitteen teemaa Lopeta tilin seuraaminen\? - Poista tuuttaus\? + Poista tuuttaus\? Mikä on instanssi\? Kopioi linkki Avaa selaimessa @@ -22,14 +22,14 @@ 30 minuuttia 5 minuuttia Lisää hashtag - Ei kuvausta + Ei kuvausta CC-BY-SA 4.0 CC-BY 4.0 Lataus epäonnistui - Avaa tuuttaus + Avaa tuuttaus Järjestelmän oletus Emojien tyyli - Lähetetään tuuttausta… + Lähetetään tuuttausta… Tallennetaanko luonnoksena\? Lukitse tili Lisää tili @@ -52,10 +52,10 @@ Lataa media Näytä suosikit Lisää välilehti - Ajasta tuuttaus + Ajasta tuuttaus Emoji-näppäimistö Sisältövaroitus - Ajastetut tuuttaukset + Ajastetut tuuttaukset Muokkaa profiilia Piilota media Ota kuva @@ -65,9 +65,9 @@ Estetyt tilit Tiliasetukset Kirjaudu ulos - Näytä vähemmän - Näytä lisää - Media piilotettu + Näytä vähemmän + Näytä lisää + Media piilotettu Seuraamispyynnöt Estetyt tilit Mykistetyt tilit @@ -93,14 +93,14 @@ Listat Päivitä Poista - Audio - Video - Kuvat + Audio + Video + Kuvat Tietoja Välityspalvelin Vain seuraajat Julkinen - Välilehdet + Välilehdet Kieli Selain Musta @@ -152,17 +152,17 @@ Seuraa TUUTAA! Tuuttaus - Ajastetut tuuttaukset + Ajastetut tuuttaukset Vastaa - \@%s + \@%s Lisenssit Luonnokset Suosikit Kirjanmerkit Seuraajat Seurataan - Kiinnitetty - Julkaisut + Kiinnitetty + Julkaisut Välilehdet Paikallinen Ilmoitukset diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4707dc17..a7f320a4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,14 +9,14 @@ Une erreur d’autorisation inconnue s’est produite. Authentification refusée. Impossible de récupérer le jeton d’authentification. - Votre pouet est trop long ! + Votre message est trop long ! Le fichier doit avoir moins de 8 Mo. Les fichiers vidéos doivent avoir moins de 40 Mo. Ce type de fichier ne peut pas être téléversé. Le fichier ne peut pas être ouvert. Permission requise pour lire le média. Permission requise pour enregistrer le média. - Un même pouet ne peut contenir à la fois une vidéo et une image. + Un même message ne peut contenir à la fois une vidéo et une image. Échec d’envoi du média. Erreur lors de l’envoi du message. Accueil @@ -25,10 +25,10 @@ Global Messages directs Onglets - Pouet - Messages - Pouets & réponses - Épinglés + Fil + Messages + Pouets & réponses + Épinglés Abonnements Abonné·e·s Favoris @@ -38,19 +38,19 @@ Modifier votre profil Brouillons Licences - \@%s - %s a partagé - Contenu sensible - Média caché - Cliquer pour voir - Voir plus - Voir moins - Déplier - Replier + \@%s + %s a partagé + Contenu sensible + Média caché + Cliquer pour voir + Voir plus + Voir moins + Déplier + Replier Rien ici. Il n’y a aucun pouet ici pour l’instant. Glissez vers le bas pour actualiser ! - %s a partagé votre pouet - %s a ajouté votre pouet à ses favoris + %s a partagé votre message + %s a ajouté votre message à ses favoris %s vous suit Signaler @%s Commentaires additionnels \? @@ -102,7 +102,7 @@ Refuser Rechercher Brouillons - Visibilité du pouet + Visibilité du message Contenu sensible Clavier d’émojis Ajouter un onglet @@ -122,14 +122,14 @@ Partager comme … Télécharger le média Téléchargement du média - Partager le lien de votre pouet avec… - Partager le pouet avec… + Partager le lien du message avec… + Partager le mesage avec… Partager l’image avec … Envoyé ! Le compte est débloqué Le compte n’est plus masqué - Envoyé ! - Réponse envoyée avec succès. + Envoyé ! + Réponse envoyée avec succès. Quelle instance ? Quoi de neuf ? Contenu sensible @@ -156,7 +156,7 @@ Télécharger Révoquer la demande d’abonnement ? Ne plus suivre ce compte ? - Supprimer ce pouet ? + Supprimer ce pouet ? Public : afficher dans les fils publics Non listé : ne pas afficher dans les fils publics Abonné·e·s uniquement : seul·e·s vos abonné·e·s verront vos statuts @@ -170,8 +170,8 @@ Me notifier quand on me mentionne on vient de me suivre - mes pouets sont partagés - mes pouets sont mis en favoris + mes messages sont partagés + mes messages sont mis en favoris Apparence Thème de l’application Fils chronologiques @@ -185,8 +185,8 @@ Utiliser le navigateur intégré Masquer le bouton de composition lors du défilement Langue - Filtrage des fils - Onglets + Filtrage des fils + Onglets Afficher les partages Afficher les réponses Montrer les miniatures des médias @@ -202,20 +202,20 @@ Public Non listé Abonné·e·s uniquement - Taille du texte pour les statuts - Plus petit - Petit - Moyen - Grand - Plus grand + Taille du texte pour les statuts + Plus petit + Petit + Moyen + Grand + Plus grand Nouvelles mentions Notifications pour les nouvelles mentions Nouveaux abonnés Notifications pour les nouveaux abonnés Partages - Notifications quand vos pouets sont partagés + Notifications quand vos messages sont partagés Favoris - Notifications quand vos pouets sont mis en favoris + Notifications quand vos messages sont mis en favoris %s vous a mentionné %1$s, %2$s, %3$s et %4$d autres %1$s, %2$s et %3$s @@ -243,10 +243,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Profil de Tusky - Partager le contenu du pouet - Partager le lien du pouet - Images - Vidéo + Partager le contenu du pouet + Partager le lien du pouet + Images + Vidéo Demande d’abonnement effectuée en %da @@ -297,11 +297,11 @@ Verrouiller le compte Vous devez approuvez manuellement les abonnements Enregistrer comme brouillon ? - Envoi du pouet… - Erreur lors de l’envoi du pouet - Envoi des pouets - Envoi annulé - Une copie du pouet a été sauvegardée dans vos brouillons + Envoi du pouet… + Erreur lors de l’envoi du pouet + Envoi des pouets + Envoi annulé + Une copie du pouet a été sauvegardée dans vos brouillons Écrire Votre instance %s n’a pas d’émojis personnalisés Copié dans le presse-papier @@ -309,8 +309,8 @@ Par défaut du système Vous devez commencer par télécharger ces jeux d’émojis Recherche en cours… - Déplier/replier tout les statuts - Ouvrir le pouet + Déplier/replier tout les statuts + Ouvrir le pouet Un redémarrage de l’application est nécessaire Vous devrez redémarrer Tusky pour appliquer ces modifications Plus tard @@ -352,15 +352,15 @@ maximum de %1$d onglet atteint maximum de %1$d onglets atteint - Média : %s + Média : %s - Avertissement : %s + Avertissement : %s - Pas de description + Pas de description - Reblogué + Reblogué - Mis en favoris + Mis en favoris Public @@ -374,12 +374,12 @@ Nettoyer Filtrer Appliquer - Écrire un pouet + Écrire un message Écrire Afficher l\'indicateur de robots Désirez-vous nettoyer toutes vos notifications de façon permanente \? Effacer et ré-écrire - Effacer et ré-écrire ce pouet \? + Effacer et ré-écrire ce pouet \? Termina à %s Terminé Voter @@ -389,19 +389,19 @@ Un sondage que vous avez créé est terminé %d jour restant - %d jours restant + %d jours restants %d heure restant - %d heures restant + %d heures restantes %d minute restant - %d minutes restant + %d minutes restantes %d seconde restant - %d secondes restant + %d secondes restantes Activer l’animation des avatars Actions pour l’image %s @@ -420,7 +420,7 @@ Commentaires additionnels Transférer à %s Échec du signalement - Échec de récupération des statuts + Échec de récupération des statuts Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous : Ê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. Terminé @@ -430,7 +430,7 @@ Un mot-clé ou une phrase alphanumérique sera appliqué·e seulement s’il ou elle correspond au mot entier Comptes Échec de la recherche - Toujours ouvrir les pouets avec un contenu sensible + Toujours ouvrir les messages ayant un contenu sensible Ajouter un sondage Sondage 5 minutes @@ -444,22 +444,22 @@ Choix multiples Choix %d Éditer - Pouets planifiés + Pouets planifiés Éditer - Pouets programmés - Planifier le pouet + Pouets programmés + Planifier le pouet Réinitialiser Erreur lors de la recherche du post %s Propulsé par Tusky Signets Marquer comme signet Signets - Ajouté aux signets + Ajouté aux signets Sélectionner la liste Liste Les fichiers audio doivent avoir moins de 40 Mo. Vous n’avez aucun brouillon. - Vous n’avez aucun pouet planifié. + Vous n’avez aucun pouet planifié. L’intervalle minimum de planification sur Mastodon est de 5 minutes. Demandes d\'abonnement Bloquer @%s \? @@ -496,28 +496,28 @@ Votre note privée sur ce compte Il n’y a pas d’annonce. Annonces - Certaines informations susceptibles d\'affecter votre bien-être mental seront cachées. Il s\'agit : + Certaines informations susceptibles d’affecter votre bien-être mental seront cachées. Il s’agit : \n \n - des notifications de favoris, de partage et de suivi -\n - du compte des favoris/partages sur les pouets +\n - du nombre des favoris/partages des messages \n - des statistiques sur les profils \n -\n Les notifications \"push\" ne seront pas affectées, mais vous pouvez revoir vos préférences de notification manuellement. - une personne que je suis a publié un nouveau pouet +\n Les notifications « push » ne seront pas affectées, mais vous pouvez revoir vos préférences de notification manuellement. + un compte auquel je suis abonné·e a publié un nouveau message %s vient de publier Examiner les notifications - Nouveaux pouets + Nouveaux messages - Vous ne pouvez pas téléverser plus de %1$d pièce jointe. + Vous ne pouvez pas téléverser plus d’ %1$d pièce jointe. Vous ne pouvez pas téléverser plus de %1$d pièces jointes. Bien-être - Notifications quand quelqu\'un que vous suivez publie un nouveau pouet - Limiter les notifications de la timeline + Notifications quand quelqu’un que vous suivez publie un nouveau message + Limiter les notifications du fil Cacher les statistiques quantitatives sur les profils Cacher les statistiques quantitatives sur les messages Supprimer le marque-page - Pièces jointes + Pièces jointes Voulez-vous vraiment supprimer la liste %s \? S’abonner Supprimer cette conversation \? @@ -527,10 +527,17 @@ Indéfinie Se désabonner Supprimer la conversation - Audio + Audio Demander confirmation avant de mettre en favoris - Le message auquel répondait ce brouillon a été supprimé - Échec d’envoi du pouet ! - Bien que votre compte ne soit pas verrouillé, l\'équipe de %1$s a pensé que vous voudriez valider manuellement les demandes de suivi provenant de ces comptes. + Le message auquel répondait ce brouillon a été supprimé + Échec d’envoi du pouet ! + 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. Échec du chargement des informations de réponse + 30 jours + 60 jours + 90 jours + 365 jours + 14 jours + 180 jours + Rédiger un message \ No newline at end of file diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml index 69cc8b7d..97ad4052 100644 --- a/app/src/main/res/values-fy/strings.xml +++ b/app/src/main/res/values-fy/strings.xml @@ -6,10 +6,10 @@ Emoji styl Nei it klemboerd kopiearre Gearstelle - Ferstjoeren ôfbrutsen - Toots oan it ferstjoeren - Flater by it ferstjoeren fan toot - Toot oan it ferstjoeren… + Ferstjoeren ôfbrutsen + Toots oan it ferstjoeren + Flater by it ferstjoeren fan toot + Toot oan it ferstjoeren… Skets bewarje\? Fuortsmite Ûnderskrift pleatse @@ -42,12 +42,12 @@ oer %dm oer %dh oer %dd - Taheaksels - Lûd - Fideo - Ôfbyldingen - Keppeling nei toot diele - Ynhâld fan toot diele + Taheaksels + Lûd + Fideo + Ôfbyldingen + Keppeling nei toot diele + Ynhâld fan toot diele Tusky %s Oer @@ -60,11 +60,11 @@ Favoriten Folgfersyken Nije Folgers - Grutst - Grut - Gewoan - Lyts - Lytst + Grutst + Grut + Gewoan + Lyts + Lytst Allinnich folgers Iepenbier Ûnder @@ -78,7 +78,7 @@ Proksje Media foarfertoaningen delhelje Reaksjes sjen litte - Ljepblêden + Ljepblêden Taal Webblêder Systeem Opmaak Brûke @@ -104,8 +104,8 @@ \@%s blokkearje\? Folsleine domein ferbergje Dit petear fuortsmite\? - Dizze toot fuortsmite en opnij opstelle\? - Dizze toot fuortsmite\? + Dizze toot fuortsmite en opnij opstelle\? + Dizze toot fuortsmite\? Dit account net mear folgje\? Folgfersyk ynlûke\? Delhelje @@ -117,15 +117,15 @@ Sykje… Ynhâld warskôging Wat bard der\? - Reaksje mei sukses ferstjoerd. - Ferstjoerd! + Reaksje mei sukses ferstjoerd. + Ferstjoerd! %s net mear ferburgen Brûker net mear negearre Brûker net mear blokkearre Ferstjoerd! Media ferstjoere nei… - Toot ferstjoere nei… - Toot URL ferstjoere nei… + Toot ferstjoere nei… + Toot URL ferstjoere nei… Media oan it delheljen Media delhelje Diele as… @@ -137,11 +137,11 @@ Favoriten besjen Keppelingen Ljepblêd Tafoegje - Toot ynplanne + Toot ynplanne Emoji toetseboerd Ynhâld warskôging Toot sichtberheid - Ynplanne toots + Ynplanne toots Sketsen Sykje Net akseptearje @@ -206,16 +206,16 @@ %s hat jo toot as favoryt oanmurken Hjir is neat. Lûk nei ûnderen om te ferfarskjen! Hjir is neat. - Yntearre - Ûttearre - Minder sjen litte - Mear sjen litte - Klik om te besjen - Media ferburgen - Gefoelige ynhâld - \@%s + Yntearre + Ûttearre + Minder sjen litte + Mear sjen litte + Klik om te besjen + Media ferburgen + Gefoelige ynhâld + \@%s Lisinsjes - Ynplanne toots + Ynplanne toots Sketsen Jo profyl oanpasse Folgfersyken @@ -226,9 +226,9 @@ Favoriten Folgers Folget - Fêstset - Mei reaksjes - Berjochten + Fêstset + Mei reaksjes + Berjochten Toot Ljepblêden Direkte Berjochten diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 9410b503..9342edf9 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -1,14 +1,14 @@ - Leathnaigh - Taispeáin Níos Lú - Taispeáin Níos Mó - Cliceáil chun amharc - Meáin i bhfolach - Ábhar íogair - threisigh %s + Leathnaigh + Taispeáin Níos Lú + Taispeáin Níos Mó + Cliceáil chun amharc + Meáin i bhfolach + Ábhar íogair + threisigh %s Ceadúnais - Tútanna sceidealta + Tútanna sceidealta Cuir do phróifíl in eagar Lean Iarrataí Fearainn i bhfolach @@ -17,8 +17,8 @@ Leabharmharcanna Leanúna Leanúna - Greamaithe - Le freagraí + Greamaithe + Le freagraí Tút Teachtaireachtaí Díreacha Cónaidhme @@ -64,8 +64,8 @@ Bloc @%s\? Folaigh an fearann iomlán An bhfuil tú cinnte gur mhaith leat gach %s a bhac\? Ní fheicfidh tú ábhar ón bhfearann sin in aon amlínte poiblí ná i d’fhógraí. Bainfear do leanúna ón bhfearann sin. - An tút seo a scriosadh agus a dhréachtú\? - Scrios an tút seo\? + An tút seo a scriosadh agus a dhréachtú\? + Scrios an tút seo\? An cuntas seo a scaoileadh\? An iarraidh seo a leanas a chúlghairm\? Íoslódáil @@ -81,15 +81,15 @@ Rabhadh ábhair Cad atá ag tarlú\? Cén cás\? - Cuireadh an freagra go rathúil. - Seolta! + Cuireadh an freagra go rathúil. + Seolta! %s neamhcheangailte Úsáideoir gan trácht Úsáideoir gan bhac Seolta! Comhroinn na meáin le… - Comhroinn tút chuig… - Comhroinn URL tút chuig… + Comhroinn tút chuig… + Comhroinn URL tút chuig… Meáin íoslódála Íoslódáil na meáin Comhroinn mar … @@ -104,11 +104,11 @@ Buaicphointí Naisc ghréasáin Cuir Tab leis - Tút a sceidealú + Tút a sceidealú Méarchlár Emoji Rabhadh ábhair Infheictheacht tút - Tútanna sceidealta + Tútanna sceidealta Dréachtaí Diúltaigh Glac @@ -182,9 +182,9 @@ Theip ar fhíordheimhniú leis an gcás sin. Cad is sampla ann\? Logáil isteach le Mastodon - Íomhánna - Comhroinn nasc le tút - Comhroinn ábhar na tút + Íomhánna + Comhroinn nasc le tút + Comhroinn ábhar na tút Próifíl Tusky Is bogearraí foinse oscailte agus saor in aisce é Tusky. Tá sé ceadúnaithe faoi Leagan 3. Ceadúnas Poiblí Ginearálta GNU 3. Is féidir leat an ceadúnas a fheiceáil anseo: https://www.gnu.org/licenses/gpl-3.0.en.html Cumhachtaithe ag Tusky @@ -209,12 +209,12 @@ Leantóirí Nua Fógraí faoi luanna nua Tagairtí Nua - Is mó - Móra - Mheán - Beag - Lúide - Méid an téacs stádais + Is mó + Móra + Mheán + Beag + Lúide + Méid an téacs stádais Leantóirí-amháin Neamhliostaithe Poiblí @@ -233,7 +233,7 @@ Íoslódáil réamhamharcanna na meán Taispeáin freagraí Taispeáin borradh - Scagadh amlíne + Scagadh amlíne Taispeáin grádáin ildaite do na meáin i bhfolach Beochan abhatár GIF Taispeáin táscaire do róbónna @@ -251,9 +251,9 @@ Chuir %s borradh faoi do tút Níl aon rud anseo. Tarraingt anuas chun athnuachan a dhéanamh! Níl aon rud anseo. - Dlúth + Dlúth Nuair atá an eochairfhocal nó an frása alfa-uimhriúil amháin, ní chuirfear i bhfeidhm é ach má oireann sé don fhocal iomlán - Tútanna + Tútanna Bhí %s i bhfabhar do tút Unmute %s Comhrá tost @@ -270,7 +270,7 @@ \n https://chinwag.org Tuarascálacha ar fhabhtanna & iarratais ar ghnéithe: \n https://git.chinwag.org/chinwag/chinwag-android/issues - Físeán + Físeán Lean iarrtha Leanann tú Taispeáin ábhar íogair i gcónaí @@ -287,7 +287,7 @@ Cuir scagaire in eagar Bain Amlínte poiblí - Gach stádas a leathnú/a thit amach + Gach stádas a leathnú/a thit amach Beidh ort Tusky a atosú chun na hathruithe seo a chur i bhfeidhm Sraith emoji reatha Google Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Tusky: @@ -307,7 +307,7 @@ D\'imigh %d nóiméad D\'imigh %d nóiméad - Theip ar stádas a fháil + Theip ar stádas a fháil Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos: Cuir Cuntas Mastodon nua leis Liostaigh amlíne @@ -332,11 +332,11 @@ Cuntas glasála Éilíonn ort leanúna a cheadú de láimh Sábháil dréacht\? - Tút a sheoladh… - Earráid agus an tút á sheoladh - Tútanna a sheoladh - Seoladh curtha ar ceal - Sábháladh cóip den tút ar do dhréachtaí + Tút a sheoladh… + Earráid agus an tút á sheoladh + Tútanna a sheoladh + Seoladh curtha ar ceal + Sábháladh cóip den tút ar do dhréachtaí Cum Níl aon emojis saincheaptha ag do shampla %s Cóipeáladh chuig an gearrthaisce @@ -344,7 +344,7 @@ Réamhshocrú an chórais Beidh ort na tacair emoji seo a íoslódáil ar dtús Amharc taibhithe… - Oscail tút + Oscail tút Atosú aip de dhíth Níos déanaí Atosaigh @@ -380,11 +380,11 @@ Ainmnithe ag %1$s agus %2$s %1$s, %2$s agus %3$d níos mó - Meáin: %s - Rabhadh ábhair: %s - Gan tuairisc - Ainmnithe - Leabharmharcáilte + Meáin: %s + Rabhadh ábhair: %s + Gan tuairisc + Ainmnithe + Leabharmharcáilte Poiblí Neamhliostaithe Leanúna @@ -464,17 +464,17 @@ Cuir in Eagar Earráid agus an post á lorg %s Níl aon dréachtaí agat. - Níl aon stádas sceidealta agat. + Níl aon stádas sceidealta agat. Tá eatramh sceidealaithe íosta 5 nóiméad ag Mastodon. Taispeáin réamhamhairc nasc in amlínte Taispeáin dialóg dearbhaithe sula ndéantar borradh faoi Cluaisíní - \@%s + \@%s Tost %s Unmute %s Comhrá unmute Tost @%s\? - Cluaisíní + Cluaisíní in %dy in %dd in %dh @@ -490,6 +490,6 @@ CC-BY-SA 4.0 Unpin %1$s - Reblogged + Reblogged Hashtags \ No newline at end of file diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 2dd5c4b9..bf2a2d52 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -55,12 +55,12 @@ Cuir crìoch air an fho-sgrìobhadh Fo-sgrìobh Beòthaich na h-Emojis gnàthaichte - Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh + Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh Chaidh an dreach a sguabadh às Cha deach leinn fiosrachadh na freagairte a luchdadh - Cha b’ urrainn dhuinn an dùd a chur! - Ceanglachain - Fuaim + Cha b’ urrainn dhuinn am post a chur! + Ceanglachain + Fuaim A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? 7 làithean 3 làithean @@ -81,12 +81,12 @@ Falaich an stadastaireachd àireamhail air postaichean Cuingich na brathan mun loidhne-ama Thoir sùil air na brathan - Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: -\n -\n - Brathan air annsachdan/brosnachaidhean/leantainn -\n - Cunntas nan annsachdan/brosnachaidhean air postaichean -\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean -\n + Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: +\n +\n - Brathan air annsachdan/brosnachaidhean/leanntainn +\n - Cunntas nan annsachdan/brosnachaidhean air postaichean +\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean +\n \n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh. Slàinte-inntinn Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr @@ -103,8 +103,8 @@ Dì-mhùch na brathan o %s Seall còmhradh dearbhaidh mus dèan thu brosnachadh Seall ro-sheallaidhean air ceanglaichean sna loidhnichean-ama - Feumaidh co-dhiù 5 mionaidean a bhith eadar staidean sgeidealaichte air Mastodon. - Chan eil staid sam bith air an sgeideal agad. + Feumaidh co-dhiù 5 mionaidean a bhith eadar postaichean sgeidealaichte air Mastodon. + Chan eil post sam bith air an sgeideal agad. Chan eil dreachd sam bith agad. Thachair mearachd le lorg a’ phuist %s Roghainn %d @@ -117,7 +117,7 @@ Cunntasan Chaidh an cunntas a chlàradh air frithealaiche eile. A bheil thu airson lethbhreac dhen ghearan a chur dha-san gun ainm cuideachd\? Thèid do ghearan a chur gu maor an fhrithealaiche agad. ’S urrainn dhut mìneachadh a sholar air carson a tha thu a’ gearan mun chunntas gu h-ìosal: - Cha b’ urrainn dhuinn na staidean fhaighinn + Cha b’ urrainn dhuinn na postaichean fhaighinn Cha b’ urrainn dhuinn do ghearan a chlàradh Sìn air adhart gu %s Beachdan a bharrachd @@ -180,12 +180,12 @@ Ainm na liosta Cunntas-bheachd le roghainnean: %1$s, %2$s, %3$s, %4$s; %5$s Dìreach - ’Na chomharra-lìn - ’Na annsachd - Air ath-bhlogadh - Gun tuairisgeul - Rabhadh susbainte: %s - Meadhan: %s + ’Na chomharra-lìn + ’Na annsachd + Air ath-bhlogadh + Gun tuairisgeul + Rabhadh susbainte: %s + Meadhan: %s ràinig thu na tha ceadaichte dhe %1$d taba ràinig thu na tha ceadaichte dhe %1$d thaba @@ -233,25 +233,25 @@ Uaireigin eile Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs Feumaidh tu an aplacaid ath-thòiseachadh - Fosgail am post - Leudaich/Co-theannaich gach staid + Fosgail am post + Leudaich/Co-theannaich gach post ’Ga lorg… Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach Bun-roghainn an t-siostaim Stoidhle nan Emojis Chaidh lethbhreac dheth a chur air an stòr-bhòrd Chan eil Emojis gnàthaichte aig an ionstans %s agad - Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd - Chaidh sgur dhen chur - A’ cur nam post - Mearachd a’ cur a’ phuist - A’ cur a’ phuist… + Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd + Chaidh sgur dhen chur + A’ cur nam post + Mearachd a’ cur a’ phuist + A’ cur a’ phuist… A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas Suidhidh am fo-thiotal - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn \n(%d caractar(an) air a char as fhaide) Cha deach leinn am fo-thiotal a shuidheachadh @@ -271,29 +271,29 @@ An abairt ri chriathradh Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin Leudaich postaichean ris a bheil rabhadh susbainte an-còmhnaidh - Co-roinn ceangal dhan phost - Co-roinn susbaint a’ phuist + Co-roinn ceangal dhan phost + Co-roinn susbaint a’ phuist ’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html Brathan nuair a thèid post agad a chomharrachadh ’na annsachd Brathan nuair a thèid post agad brosnachadh - A bheil thu airson am post seo a sguabadh às is dreachd ùr a dhèanamh air\? - A bheil thu airson am post seo a sguabadh às\? - ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! + A bheil thu airson am post seo a sguabadh às is dreachd ùr a dhèanamh air\? + A bheil thu airson am post seo a sguabadh às\? + ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! \n \nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. \n -\n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. +\n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. \n \nGheibh thu barrachd fiosrachaidh air joinmastodon.org. - Co-roinn am post le… - Co-roinn URL a’ phuist le… - Cuir post air an sgeideal + Co-roinn am post le… + Co-roinn URL a’ phuist le… + Cuir post air an sgeideal Faicsinneachd a’ phuist - Postaichean air an sgeideal + Postaichean air an sgeideal Chuir %s am post agad ris na h-annsachdan Bhrosnaich %s am post agad - Postaichean air an sgeideal - Post + Postaichean air an sgeideal + Snàithlean Mearachd a’ cur a’ phuist. Dì-mhùch %s Tagaichean hais @@ -324,8 +324,8 @@ an ceann %dl an ceann %db Iarrar leantainn orm - Videothan - Dealbhan + Videothan + Dealbhan Pròifil Tusky Aithrisean air bugaichean ⁊ iarrtasan air gleusan: \n https://github.com/tuskyapp/Tusky/issues @@ -353,12 +353,12 @@ Luchd-leantainn ùr Brathan mu iomraidhean ùra Iomraidhean ùra - As motha - Mòr - Meadhanach - Beag - As lugha - Meud teacsa na staid + As motha + Mòr + Meadhanach + Beag + As lugha + Meud teacsa nam post Luchd-leantainn a-mhàin Neo-liostaichte Poblach @@ -376,8 +376,8 @@ Progsaidh Luchdaich a-nuas ro-sheallaidhean air meadhanan Seall na freagairtean - Tabaichean - Criathradh na loidhne-ama + Tabaichean + Criathradh na loidhne-ama Seall caiseadan dathte an àite meadhanan falaichte Beothaich avataran GIF Seall taisbeanair do bhotaichean @@ -423,8 +423,8 @@ Avatar Chan eil toradh ann Cò an t-ionstans\? - Chaidh an fhreagairt a chur. - Chaidh a chur! + Chaidh an fhreagairt a chur. + Chaidh a chur! Chan eil %s falaichte tuilleadh Chaidh an cleachdaiche dhì-mhùchadh Chaidh an cleachdaiche a dhì-bhacadh @@ -485,15 +485,15 @@ Lean %s ort Chan eil dad an-seo. Tarraing a-nuas airson ath-nuadhachadh! Chan eil dad an-seo. - Co-theannaich - Leudaich - Seall nas lugha dheth - Seall barrachd dheth - Briog air gus a shealltainn - Meadhanan falaichte - Susbaint fhrionasach - ’Ga bhrosnachadh le %s - \@%s + Co-theannaich + Leudaich + Seall nas lugha dheth + Seall barrachd dheth + Briog air gus a shealltainn + Meadhanan falaichte + Susbaint fhrionasach + ’Ga bhrosnachadh le %s + \@%s Ceadachasan Iarrtasan leantainn Àrainnean falaichte @@ -502,16 +502,16 @@ Comharran-lìn Luchd-leantainn A’ leantainn air - Prìnichte - Le freagairt - Postaichean + Prìnichte + Le freagairt + Postaichean Tabaichean Teachdaireachdan dìreach Co-naisgte Ionadail Dachaigh Dh’fhàillig leis an luchdadh suas. - Chan urrainn dhut an dà chuid dealbhan is videothan a cheangal ris an aon staid. + Chan urrainn dhut an dà chuid dealbhan is videothan a cheangal ris an aon phost. Tha feum air cead gus meadhanan a stòradh. Tha feum air cead gus meadhanan a leughadh. Cha b’ urrainn dhuinn am faidhle sin fhosgladh. @@ -519,7 +519,7 @@ Feumaidh faidhlichean fuaime a bhith nas lugha na 40MB. Feumaidh faidhlichean video a bhith nas lugha na 40MB. Feumaidh am faidhle a bhith nas lugha na 8MB. - Tha an staid ro fhada! + Tha am post ro fhada! Cha deach leinn tòcan clàraidh a-steach fhaighinn. Chaidh an t-ùghdarrachadh a dhiùltadh. Thachair mearachd leis an ùghdarrachadh nach do dh’aithnich sinn. @@ -534,4 +534,11 @@ Sguab às an còmhradh Thoir an comharra-lìn air falbh Ceall còmhradh dearbhaidh mus dèid post ’na annsachd + 30 latha + 90 latha + 180 latha + 365 latha + 14 làithean + 60 latha + Sgrìobh post \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index dbf0f5b8..bfabe4c8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -1,8 +1,8 @@ Aviso sobre o contido - Visibilidade do toot - Toots programados + Visibilidade da publicación + Toots programados Borradores Buscar Rexeitar @@ -54,7 +54,7 @@ Seguir Tes a certeza de que queres desconectar a conta %1$s\? Desconectar - Conecta con Mastodon + Accede con Mastodon Redactar Máis Eliminar favorito @@ -69,22 +69,22 @@ %s publicou agora %s solicitou seguirte %s seguiute - %s fixo favorito o teu toot - %s promoveu o teu toot + %s fixo favorita a publicación + %s promoveu a túa publicación Nada por aquí. Arrastra hacia abaixo para actualizar! Nada por aquí. - Pregar - Expandir - Amosar menos - Amosar máis - Click para ver - Multimedia agochado - Contido sensible - %s promoveu - \@%s + Pregar + Expandir + Ver menos + Ver máis + Click para ver + Multimedia agochado + Contido sensible + %s promoveu + \@%s Licenzas Anuncios - Toots programados + Toots programados Borradores Edita o teu perfil Solicitudes de seguimento @@ -95,19 +95,19 @@ Favoritos Seguidoras Segue - Fixado - Con resposta - Publicacións - Toot + Fixado + Con resposta + Publicacións + Fío Lapelas Mensaxes directas Federada Local Notificacións Inicio - Erro ao enviar o toot. + Erro ao enviar a publicación. Fallou a subida. - As imaxes e vídeo non poden engadirse simultáneamente a un mesmo estado. + As imaxes e vídeo non poden engadirse simultáneamente á mesma publicación. Requírese o permiso de almacenaxe do multimedia. Requírese o permiso de lectura do multimedia. Non se puido abrir o ficheiro. @@ -115,8 +115,8 @@ Os ficheiros de audio teñen que ser menores de 40MB. Os ficheiros de vídeo teñen que ser menores de 40MB. O ficheiro debe ser menor de 8MB. - O estado é demasiado longo! - Fallou a obtención do token de conexión. + A publicación é demasiado longa! + Fallou a obtención do token de acceso. A autorización foi rexeitada. Aconteceu un erro non identificado de autorización. Non se atopou un navegador para utilizar. @@ -128,7 +128,7 @@ %1$s Desubscribir Subscribir - Eliminouse o toot para o que redactaches a resposta + Eliminouse o toot para o que redactaches a resposta Sen límite Duración Enquisa @@ -138,7 +138,7 @@ Contas A conta pertence a outro servidor. Queres enviar unha copia anónima da denuncia alí tamén\? A denuncia vaise enviar á moderación do teu servidor. Podes engadir algunha explicación ou razón pola que estás denunciando a conta: - Fallou a obtención dos estados + Fallou a obtención dos estados Fallo ao realizar a denuncia Reenviar a %s Comentarios adicionais @@ -179,7 +179,7 @@ Accións para a imaxe %s Tes a certeza de que queres borrar permanentemente todas as notificacións\? Redactar - Redactar Toot + Redactar publicación Aplicar Filtrar Despexar @@ -194,12 +194,12 @@ Seguidoras Non listado Público - Marcado - Favorecido - Repetido - Sen descrición - Aviso sobre o contido: %s - Multimedia: %s + Marcado + Favorecido + Repetido + Sen descrición + Aviso sobre o contido: %s + Multimedia: %s acadouse o máximo de %1$d lapela acadouse o máximo de %1$d lapelas @@ -241,8 +241,8 @@ Máis tarde Deberás reiniciar Tusky para aplicar os cambios Require reiniciar app - Abrir toot - Expandir/Pregar tódolos estados + Abrir toot + Expandir/Pregar tódolos estados Realizando a busca… Deberás descargar primeiro estos conxuntos de emojis Por defecto no sistema @@ -250,11 +250,11 @@ Copiado ao portapapeis A túa instancia %s non ten emojis personalizados Redactar - Gardouse unha copia do toot nos borradores - Envío cancelado - Enviando Toots - Erro ao enviar o toot - Enviando Toot… + Gardouse unha copia do toot nos borradores + Envío cancelado + Enviando Toots + Erro ao enviar o toot + Enviando Toot… Gardar borrador\? Require que aprobes manualmente as seguidoras Bloquear conta @@ -293,7 +293,7 @@ cargar máis Respondendo a @%s Multimedia - Expandir sempre toots marcados con avisos sobre o contido + Despregar sempre publicacións marcadas con avisos sobre o contido Mostrar sempre contido sensible Séguete %ds @@ -307,12 +307,12 @@ en %dd en %dy Seguimento solicitado - Anexos - Audio - Vídeo - Imaxes - Compartir ligazón ao toot - Compartir contido do toot + Anexos + Audio + Vídeo + Imaxes + Compartir ligazón ao toot + Compartir contido do toot Perfil de Tusky Informar de fallos e solicitar funcións: \n https://github.com/tuskyapp/Tusky/issues @@ -331,13 +331,13 @@ %1$s, %2$s, e %3$s %1$s, %2$s, %3$s e %4$d outras %s mencionoute - Notificacións cando alguén a quen estás subscrita publica un novo toot - Novos toots + Notificacións cando alguén a quen estás subscrita publica novo contido + Novas publicacións Notificacións cando rematan as enquisas Enquisas - Notificacións cando os teus toots son marcados como favoritos + Notificacións cando as túas publicacións son favorecidas Favoritos - Notificacións cando os teus toots son promovidos + Notificacións cando as túas publicacións sexan promovidas Promocións Notificación acerca de solicitudes de seguimento Solicitudes de seguimento @@ -345,12 +345,12 @@ Novas seguidoras Notificación de novas mencións Novas mencións - O máis grande - Grande - Medio - Pequeno - O máis pequeno - Tamaño do texto do estado + O máis grande + Grande + Medio + Pequeno + O máis pequeno + Tamaño do texto do estado Só seguidoras Non listado Público @@ -369,8 +369,8 @@ Descarga vista previa do multimedia Mostar respostas Mostrar promocións - Lapelas - Filtros na cronoloxía + Lapelas + Filtros na cronoloxía Animar emojis personalizados Mostra gradientes coloridos para multimedia oculto Animar avatares GIF @@ -388,7 +388,7 @@ Cronoloxías Decorado da app Aparencia - alguén a quen eu siga publique un toot + alguén a quen eu siga publique novo contido rematen as enquisas marquen un toot meu como favorito promocionen un dos meus toots @@ -411,8 +411,8 @@ Bloquear @%s\? Agochar todo o dominio Tes a certeza de querer bloquear a todo %s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas. - Eliminar e reescribir este toot\? - Eliminar este toot\? + Eliminar e reescribir esta publicación\? + Eliminar esta publicación\? Deixar de seguir esta conta\? Revogar a solicitude de seguimento\? Descargar @@ -438,7 +438,7 @@ Que contas\? Borrador eliminado Fallou a carga da información da Resposta - Fallou o envío do toot! + Fallou o envío do toot! Tes a certeza de querer eliminar a listaxe %s\? Non podes subir máis de %1$d anexo multimedia. @@ -451,7 +451,7 @@ Agocharemos algunha información que podería afectar ao teu benestar mental. Esto inclúe: \n \n- Notificacións acerca de Favoritos/Promocións/Seguimentos -\n- Número de Favoritos/Promocións nos toots +\n- Número de Favoritos/Promocións nas publicacións \n- Estatísticas de Seguidoras/Publicacións nos perfís \n \nAs notificacións tipo push non estarán afectadas, mais podes revisar as preferencias de notificacións manualmente. @@ -459,11 +459,11 @@ Nota privada acerca desta conta Benestar Agochar o título da barra de ferramentas superior - Amosar diálogo de confirmación antes de promover - Amosar vista previa das ligazóns nas cronoloxías + Pedir confirmación antes de promover + Ver vista previa das ligazóns nas cronoloxías Mastodon ten un intervalo mínimo de 5 minutos para as programacións. Non hai anuncios. - Non tes estados programados. + Non tes estados programados. Non tes borradores. Erro ao buscar publicación %s Editar @@ -478,15 +478,15 @@ 30 minutos 5 minutos En que instancia\? - Resposta enviada correctamente. - Enviado! + Resposta enviada correctamente. + Enviado! %s visible Usuaria reactivada Usuaria desbloqueada Enviado! Compartir multimedia en… - Compartir toot en… - Compartir URL do toot a… + Compartir toot en… + Compartir URL do toot a… Descargando multimedia Descargar multimedia Compartir como … @@ -505,11 +505,18 @@ Ligazóns Engadir lapela Restablecer - Programar Toot + Programar Toot Teclado Emoji Aínda que a túa conta non está bloqueada, a administración de %1$s opina que debes revisar manualmente as peticións de seguimento destas contas. Eliminar esta conversa\? Eliminar conversa Eliminar marcador Mostrar diálogo de confirmación antes de favorecer + 14 días + 30 días + 60 días + 90 días + 180 días + 365 días + Redactar publicación \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 2ed1f08f..16882504 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -9,7 +9,7 @@ प्रोफाइल एडिट करें खोज के बारे में - उत्तरों के साथ + उत्तरों के साथ अनुगामी उत्तर दें फ़ॉलो @@ -60,8 +60,8 @@ वोट बन्द है लिखें - पिन की गई - पोस्ट + पिन की गई + पोस्ट घर टस्की में निम्नलिखित ओपन सोर्स परियोजनाओं से कोड और संपत्ति हैं: इन परिवर्तनों को लागू करने के लिए आपको टस्की को पुनः आरंभ करना होगा @@ -79,11 +79,11 @@ %1$s, %2$s, %3$s तथा %4$d अन्य लोग जनमत पसंदीदा - सबसे बड़ा - बड़ा - मध्यम - छोटा - सबसे छोटा + सबसे बड़ा + बड़ा + मध्यम + छोटा + सबसे छोटा असूचीबद्ध सार्वजनिक सबसे नीचे @@ -94,7 +94,7 @@ डिफ़ॉल्ट पोस्ट गोपनीयता मीडिया पूर्वावलोकन डाउनलोड करें जवाब दिखाएँ - टैब्स + टैब्स छिपे हुए मीडिया के लिए रंगीन ग्रेडिएंट दिखाएं बॉट्स के लिए संकेतक दिखाएं भाषा @@ -132,8 +132,8 @@ प्रदर्शित होने वाला नाम विषय वस्तु चेतावनी क्या हो रहा है\? - उत्तर सफलतापूर्वक भेजा गया। - भेज दिया! + उत्तर सफलतापूर्वक भेजा गया। + भेज दिया! भेज दिया! मीडिया डाउनलोड हो रहा है मीडिया डाउनलोड करें @@ -146,11 +146,11 @@ हैशटैग ज़िक्र ऐड टैब - अनुसूची टूट + अनुसूची टूट इमोजी कीबोर्ड विषय वस्तु चेतावनी टूट दृश्यता - अनुसूचित टूट + अनुसूचित टूट स्वीकार करें ड्राफ्ट अस्वीकार करें @@ -180,15 +180,15 @@ %s ने आपके टूट को बढावा दिया यहाँ कुछ नहीं। ताज़ा करने के लिए नीचे खींचो! यहाँ कुछ नहीं। - कम दिखाएं - और दिखाओ - देखने के लिए क्लिक करें - मीडिया छिपा हुआ - संवेदनशील विषय वस्तु - \@%s + कम दिखाएं + और दिखाओ + देखने के लिए क्लिक करें + मीडिया छिपा हुआ + संवेदनशील विषय वस्तु + \@%s लाइसेंस टूट - अनुसूचित टूट + अनुसूचित टूट छिपे हुए डोमेन म्यूट किए गए उपयोगकर्ता बुकमार्क @@ -229,22 +229,22 @@ हैशटैग %1$s • %2$s सूची - बुकमार्क किया हुआ + बुकमार्क किया हुआ सूची का नाम - कोई विवरण नहीं - मीडिया: %s + कोई विवरण नहीं + मीडिया: %s जानकारी जोड़ें CC-BY-SA 4.0 CC-BY 4.0 प्रोफ़ाइल मेटाडेटा डाउनलोड विफल - टूट खोलें + टूट खोलें देखने की क्रिया जारी… सिस्टम डिफ़ॉल्ट इमोजी का अंदाज - भेजना रद्द हो गया - टूट भेज रहे - टूट भेज रहे… + भेजना रद्द हो गया + टूट भेज रहे + टूट भेज रहे… लिखने को सुरक्षित करें\? खाता लॉक करें कैप्शन सेट करें @@ -279,7 +279,7 @@ प्रत्यक्ष असूचीबद्ध सार्वजनिक - विषय वस्तु चेतावनी: %s + विषय वस्तु चेतावनी: %s अधिकतम %1$d टैब तक पहुंच गऐ @@ -308,8 +308,8 @@ आपको पहले इस इमोजी सेट को डाउनलोड करना होगा क्लिपबोर्ड पर कॉपी किया गया लिखें - टूट की एक प्रति आपके ड्राफ्ट में सहेज ली गई है - टूट भेजने में त्रुटि + टूट की एक प्रति आपके ड्राफ्ट में सहेज ली गई है + टूट भेजने में त्रुटि हटाएँ हटा दें @@ -339,10 +339,10 @@ आपको फॉलो करते है फ़ॉलो करने का अनुरोध किया फ़ॉलो करने का अनुरोध किया हुआ - वीडियो - तस्वीरें - टूट का लिंक साझा करें - टूट की विषय वस्तु साझा करें + वीडियो + तस्वीरें + टूट का लिंक साझा करें + टूट की विषय वस्तु साझा करें %s ने आपका जिक्र किया जनमत खत्म होने के बारे में सूचनाएं सूचनाएं जब आपके टूट पसंद किये जाये @@ -356,7 +356,7 @@ एच टी टी पी प्रॉक्सी सक्षम करें एच टी टी पी प्रॉक्सी प्रॉक्सी - टाइमलाइन छानने का काम + टाइमलाइन छानने का काम प्रकाश टाइमलाइन एप्लिकेशन थीम @@ -368,8 +368,8 @@ उपयोगकर्ता अनम्यूट किए गए उपयोगकर्ता अनब्लॉक किए गए मीडिया को साझा करें … - टूट को साझा करें … - टूट यूआरएल को साझा करें … + टूट को साझा करें … + टूट यूआरएल को साझा करें … साझा करें … %s के रूप में खोलें %1$s डाउनलोड हो रहा है diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8a8a6753..7371cbe7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -26,9 +26,9 @@ Közvetlen üzenetek Fülek Tülk - Tülkök - Válaszokkal - Rögzített + Tülkök + Válaszokkal + Rögzített Követett Követő Kedvencek @@ -38,15 +38,15 @@ Profilod szerkesztése Piszkozatok Licenszek - \@%s - %s megtolta - Kényes tartalom - Rejtett média - Kattints a megnézéshez - Mutass többet - Mutass kevesebbet - Kibontás - Összecsukás + \@%s + %s megtolta + Kényes tartalom + Rejtett média + Kattints a megnézéshez + Mutass többet + Mutass kevesebbet + Kibontás + Összecsukás Nincs itt semmi. Üres tartalom. Húzd le a frissítéshez! %s megtolta a tülködet @@ -115,13 +115,13 @@ Link másolása Megnyitás mint %s Megosztás mint … - Tülk URL megosztása… - Tülk megosztása… + Tülk URL megosztása… + Tülk megosztása… Elküldve! Felhasználó letiltása feloldva Felhasználó némítása feloldva - Elküldve! - Válasz sikeresen elküldve. + Elküldve! + Válasz sikeresen elküldve. Melyik példány\? Mi jár a fejedben\? Tartalom figyelmeztetés @@ -146,7 +146,7 @@ Letöltés Visszavonod a követési kérelmet? Követés megszüntetése? - Törlöd ezt a tülköt? + Törlöd ezt a tülköt? Nyilvános: Tülkölés nyilvános idővonalra Listázatlan: Nem jelenik meg a nyilvános idővonalon Csak követőknek: Tülkölés csak követőknek @@ -171,8 +171,8 @@ Böngésző Linkek megnyitása applikáción belül Szerkesztés gomb elrejtése görgetés közben - Idővonal szűrése - Fülek + Idővonal szűrése + Fülek Megtolások mutatása Válaszok mutatása Média előnézet mutatása @@ -187,12 +187,12 @@ Nyilvános Listázatlan Csak követőknek - Tülkölés szöveg mérete - Legkisebb - Kicsi - Közepes - Nagy - Legnagyobb + Tülkölés szöveg mérete + Legkisebb + Kicsi + Közepes + Nagy + Legnagyobb Új említések Értesítések új említések esetén Új követők @@ -224,10 +224,10 @@ Hibajelentés & új funkciók igénylése: \n https://git.chinwag.org/chinwag/chinwag-android/issues Tusky profilja - Tülk tartalmának megosztása - Tülk linkjének megosztása - Képek - Videók + Tülk tartalmának megosztása + Tülk linkjének megosztása + Képek + Videók Követés kérelmezve Követ téged @@ -241,11 +241,11 @@ Törlés Fiók zárolása Elmented a piszkozatot\? - Tülk elküldése… - A tülk elküldése nem sikerült - Tülkök elküldése - Küldés megszakítva - A tülk másolatát elmentettük a piszkozataid közé + Tülk elküldése… + A tülk elküldése nem sikerült + Tülkök elküldése + Küldés megszakítva + A tülk másolatát elmentettük a piszkozataid közé Szerkesztés A %s szervernek nincsenek egyedi emoji-jai Vágólapra másolva @@ -253,7 +253,7 @@ Rendszer alapértelmezés Először le kell töltened ezeket az emoji készleteket Keresés… - Tülk megnyitása + Tülk megnyitása Az app újraindítása szükséges A beállítások érvényesítéséhez újra kell indítani a Tuskyt Később @@ -280,7 +280,7 @@ elérted a fülek maximális számát (%1$d) elérted a fülek maximális számát (%1$d) - Nincs leírás + Nincs leírás Nyilvános Követők @@ -290,7 +290,7 @@ Média letöltése Média letöltése Média megosztása következővel… - Törlöd és újraírod ezt a tülköt\? + Törlöd és újraírod ezt a tülköt\? befejeződött egy szavazás Szűrők Rendszer téma használata @@ -350,7 +350,7 @@ Cím beállítása Minden követődet külön engedélyezned kell - Minden tülk kibontása/összecsukása + Minden tülk kibontása/összecsukása A Google jelenlegi emodzsi készlete Megtolás az eredeti közönségnek Megtolás visszavonása @@ -367,10 +367,10 @@ %1$s %1$s, %2$s és még %3$d - Média: %s - Tartalom figyelmeztetés: %s - Megtolt - Kedvelt + Média: %s + Tartalom figyelmeztetés: %s + Megtolt + Kedvelt Listázatlan Közvetlen Szavazás válaszokkal: %1$s, %2$s, %3$s, %4$s; %5$s @@ -416,7 +416,7 @@ Egyéb megjegyzések Továbbítás neki %s Nem sikerült a bejelentés - Nem sikerült a tülkök letöltése + Nem sikerült a tülkök letöltése A bejelentést a szervered moderátorának küldjük el. Alább megadhatsz egy magyarázatot arra, hogy miért jelented be ezt a fiókot: A fiók egy másik szerverről származik. Küldjünk oda is egy anonimizált másolatot a bejelentésről\? Értesítések szűrőjének mutatása @@ -436,22 +436,22 @@ Több lehetőség Válasz %d Szerkesztés - Időzített tülkök + Időzített tülkök Szerkesztés - Időzített tülkök - Tülk Időzítése + Időzített tülkök + Tülk Időzítése Visszaállítás Nem találjuk ezt a tülköt %s Könyvjelzők Könyvjelzőzés Könyvjelzők Tusky által hajtva - Könyvjelzőzve + Könyvjelzőzve Lista kiválasztása Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. Nincs egy piszkozatod sem. - Nincs egy ütemezett tülköd sem. + Nincs egy ütemezett tülköd sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek Jóváhagyó ablak mutatása megtolás előtt @@ -484,10 +484,10 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények - A tülköt, melyre válaszul piszkozatot készítettél törölték + A tülköt, melyre válaszul piszkozatot készítettél törölték Piszkozat törölve Nem sikerült a Válasz információit betölteni - Ez a tülk nem küldődött el! + Ez a tülk nem küldődött el! Tényleg le akarod törölni a %s listát\? Nem tölthetsz fel %1$d médiacsatolmányból többet. @@ -506,8 +506,8 @@ \nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat. Végtelen Időtartam - Csatolmányok - Audio + Csatolmányok + Audio Értesítések általam követett személy új tülkjeiről Új tülkök valaki, akit követek újat tülkölt diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ae33eecc..2018dc85 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -21,26 +21,26 @@ Óskilgreind auðkenningarvilla kom upp. Heimild var hafnað. Mistókst að fá innskráningarteikn. - Stöðufærslan er of löng! + Færslan er of löng! Skráin verður að vera minni en 8MB. Myndskeiðaskrár verða að vera minni en 40MB. Þessa tegund skrár er ekki hægt að senda inn. Ekki var hægt að opna skrána. Krafist er heimilda til að lesa gögn. Krafist er heimilda til að geyma gögn. - Ekki er hægt að hengja bæði myndir og myndskeið við sömu stöðufærslu. + Ekki er hægt að hengja bæði myndir og myndskeið við sömu færslu. Sendingin mistókst. - Villa við að senda tíst. + Villa við að senda færslu. Heim Tilkynningar Staðvært Sameiginlegt Bein skilaboð Flipar - Tíst - Færslur - Með svörum - Fest + Þráður + Færslur + Með svörum + Fest Fylgist með Fylgjendur Bókamerki @@ -49,21 +49,21 @@ Falin lén Fylgjendabeiðnir Breyta notandasniðinu þínu - Áætluð tíst + Áætluð tíst Notkunarleyfi - \@%s - %s endurbirti - Viðkvæmt efni - Myndefni er falið - Ýttu til að skoða - Sýna meira - Sýna minna - Fletta út - Fella saman + \@%s + %s endurbirti + Viðkvæmt efni + Myndefni er falið + Ýttu til að skoða + Sýna meira + Sýna minna + Fletta út + Fella saman Ekkert hér. Ekkert hér. Togaðu niður til að endurhlaða! - %s endurbirti tístið þitt - %s setti tíst frá þér í eftirlæti + %s endurbirti færsluna þína + %s setti færslu frá þér í eftirlæti %s fylgist núna með þér Kæra @%s Aðrar athugasemdir\? @@ -116,11 +116,11 @@ Samþykkja Hafna Drög - Áætluð tíst - Sýnileiki tísts + Áætluð tíst + Sýnileiki færslu Aðvörun vegna efnis Lyklaborð með tjáningartáknum - Tímasetja tíst + Tímasetja tíst Frumstilla Bæta við flipa Tenglar @@ -139,15 +139,15 @@ Deila sem … Sækja myndefni Næ í myndefni - Deila slóð á tíst til… - Deila tísti með… + Deila slóð á tíst til… + Deila tísti með… Deila myndefni með… Sent! Hætt að útiloka notanda Hætt að þagga niður í notanda Hætt að fela %s - Sent! - Það tókst að senda svarið. + Sent! + Það tókst að senda svarið. Hvaða tilvik\? Hvað er í gangi hérna\? Aðvörun vegna efnis @@ -171,8 +171,8 @@ Sækja Afturkalla beiðni um að fylgjast með\? Hætta að fylgjast með þessum aðgangi\? - Eyða þessu tísti\? - Eyða og endurvinna þetta tíst\? + Eyða þessu tísti\? + Eyða og endurvinna þetta tíst\? Ertu alveg algjörlega viss um að þú viljir loka á allt %s\? Þú munt ekki sjá efni frá þessu léni í neinum opinberum tímalínum eða í tilkynningunum þínum. Fylgjendur þínir frá þessu léni verða fjarlægðir. Fela allt lénið Opinbert: Senda á opinberar tímalínur @@ -206,8 +206,8 @@ Tungumál Birta merki á róbótum Sýna hreyfingar GIF-auðkennismynda - Síun tímalínu - Flipar + Síun tímalínu + Flipar Sýna endurbirtingar Sýna svör Sækja forskoðanir á myndefni @@ -223,20 +223,20 @@ Opinbert Óskráð Einungis fylgjendur - Textastærð stöðufærslu - Minnstu - Lítið - Miðlungs - Stórt - Stærst + Textastærð stöðufærslu + Minnstu + Lítið + Miðlungs + Stórt + Stærst Nýjar tilvísanir Tilkynningar um nýjar tilvísanir Nýir fylgjendur Tilkynningar um nýja fylgjendur Endurbirtingar - Tilkynningar þegar tístin þín eru endurbirt + Tilkynningar þegar færslurnar þínar eru endurbirtar Eftirlæti - Tilkynningar þegar tístin þín eru sett í eftirlæti + Tilkynningar þegar færslurnar þínar eru settar í eftirlæti Kannanir Tilkynningar um kannanir sem er lokið %s minntist á þig @@ -256,10 +256,10 @@ Villutilkynningar og beiðnir um nýja eiginleika: \n https://git.chinwag.org/chinwag/chinwag-android/issues Notandasnið Tusky - Deila efni úr tísti - Deila tengli á tíst - Myndir - Myndskeið + Deila efni úr tísti + Deila tengli á tíst + Myndir + Myndskeið Beðið um að fylgja eftir %dár eftir %dd @@ -273,7 +273,7 @@ %ds Fylgir þér Alltaf birta myndefni sem merkt er viðkvæmt - Alltaf fletta út tístum sem eru með aðvörun vegna efnis + Alltaf fletta út færslum sem eru með aðvörun vegna efnis Gagnaskrár Svar til @%s hlaða inn fleiru @@ -310,11 +310,11 @@ Læsa notandaaðgangi Krefst þess að þú samþykkir fylgjendur handvirkt Vista drög\? - Sendi tíst… - Villa við að senda tíst - Sendi tíst - Aflýsti sendingu - Afrit af tístinu þínu hefur verið vistað drögunum þínum + Sendi tíst… + Villa við að senda tíst + Sendi tíst + Aflýsti sendingu + Afrit af tístinu þínu hefur verið vistað drögunum þínum Semja skilaboð Tilvikið þitt %s er ekki með nein sérsniðin tjáningartákn Afritað á klippispjald @@ -322,8 +322,8 @@ Sjálfgefið í kerfinu Þú þarft fyrst að ná í þessi táknmyndasett Framkvæmi uppflettingu… - Þenja út / Fella saman allar stöðufærslur - Opna tíst + Þenja út / Fella saman allar stöðufærslur + Opna tíst Endurræsing forrits er nauðsynleg Það þarf að endurræsa Tusky til að breytingarnar taki gildi Seinna @@ -358,12 +358,12 @@ hámarksfjölda %1$d flipa náð hámarksfjölda %1$d flipa náð - Myndefni: %s - Aðvörun vegna efnis: %s - Engin lýsing - Endurbloggað - Í eftirlætum - Bókamerkt + Myndefni: %s + Aðvörun vegna efnis: %s + Engin lýsing + Endurbloggað + Í eftirlætum + Bókamerkt Opinbert Óskráð Fylgjendur @@ -376,7 +376,7 @@ Hreinsa Sía Virkja - Semja tíst + Semja færslu Semja skilaboð Ertu viss um að þú viljir endanlega eyða öllum tilkynningunum þínum\? Aðgerðir fyrir mynd %s @@ -397,7 +397,7 @@ Aðrar athugasemdir Áframsenda til %s Mistókst að kæra - Mistókst að sækja stöðufærslur + Mistókst að sækja stöðufærslur Kæran verður send á umsjónarmenn vefþjónsins þíns. Þú getur gefið skýringu hér fyrir neðan á því af hverju þú ert að kæra þennan notandaaðgang: Notandaaðgangurinn er af öðrum vefþjóni. Á einnig að senda nafnlaust afrit af kærunni þangað\? Notandaaðgangar @@ -417,7 +417,7 @@ Breyta Villa við að fletta upp færslunni %s Þú ert ekki með nein drög. - Þú ert ekki með neinar áætlaðar stöðufærslur. + Þú ert ekki með neinar áætlaðar stöðufærslur. Hljóðskrár verða að vera minni en 40MB. Mastodon er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir. Fylgjendabeiðnir @@ -478,19 +478,19 @@ Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á: \n \n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir -\n - Eftirlæti/Talningu á endurbirtingum tísta +\n - Eftirlæti/Talningu á endurbirtingum færslna \n - Fylgjendur/Tölfræði færslna í notendasniðum \n \n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar. Segja upp áskrift Gerast áskrifandi Hreyfa sérsniðin tjáningartákn - Tístið sem þú gerðir drög að svari við hefur veriið fjarlægt + Tístið sem þú gerðir drög að svari við hefur veriið fjarlægt Eyddi drögum Mistókst að hlaða inn svarupplýsingum - Mistókst að senda þetta tíst! - Viðhengi - Hljóð + Mistókst að senda þetta tíst! + Viðhengi + Hljóð Ertu viss um að þú viljir eyða %s listanum\? Ótiltekið Tímalengd @@ -503,13 +503,20 @@ Takmarka tilkynningar á tímalínu Yfirfara tilkynningar Vellíðan - Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýtt tíst - Ný tíst - einhver sem ég er áskrifandi að birti nýtt tíst + Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýja færslu + Nýjar færslur + einhver sem ég er áskrifandi að birti nýja færslu %s sendi inn rétt í þessu Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum. Fjarlægja bókamerki Birta staðfestingarglugga áður en sett er í eftirlæti Eyða þessu samtali\? Eyða samtali + 30 dagar + 60 dagar + 90 dagar + 180 dagar + 365 dagar + 14 dagar + Semja færslu \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 216e0fa0..af39264f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -26,11 +26,11 @@ Messaggi Diretti Schede Toot - Post - Con risposte - Fissati in alto - Seguono - Ti seguono + Post + Con risposte + Fissati in alto + Seguiti + Seguono Preferiti Utenti silenziati Utenti bloccati @@ -38,15 +38,15 @@ Modifica il tuo profilo Bozze Licenze - \@%s - %s ha boostato - Contenuto sensibile - Media nascosto - Clicca per visualizzare - Mostra di Più - Mostra Meno - Espandi - Riduci + \@%s + %s ha boostato + Contenuto sensibile + Media nascosto + Clicca per visualizzare + Mostra di Più + Mostra Meno + Espandi + Riduci Qui non c\'è niente. Qui non c\'è niente. Trascina verso il basso per aggiornare! %s ha boostato il tuo toot @@ -120,14 +120,14 @@ Copia il link Apri come %s Condividi come … - Condividi URL del toot su… - Condividi toot su… + Condividi URL del toot su… + Condividi toot su… Condividi media su… Inviato! Utente sbloccato Utente non più silenziato - Inviato! - Risposta inviata con successo. + Inviato! + Risposta inviata con successo. Quale istanza? Cosa succede? Avviso di contenuto sensibile @@ -152,7 +152,7 @@ Scarica Revocare la richiesta di seguire? Smettere di seguire questo account? - Eliminare questo toot? + Eliminare questo toot? Pubblico: visibile sulla timeline pubblica Non Elencato: non visibile sulla timeline pubblica e locale Solo Follower: visibile solo dai tuoi follower @@ -181,8 +181,8 @@ Usa Tab Personalizzate di Chrome Nascondi il pulsante componi mentre scorri Lingua - Filtraggio della timeline - Schede + Filtraggio della timeline + Schede Mostra boost Mostra risposte Mostra anteprime media @@ -198,12 +198,12 @@ Pubblico Non elencato Solo per chi ti segue - Dimensione del testo degli stati - Piccolissimo - Piccolo - Normale - Grande - Grandissimo + Dimensione del testo degli stati + Piccolissimo + Piccolo + Normale + Grande + Grandissimo Nuove Menzioni Notifiche quando qualcuno ti menziona Nuove persone che ti seguono @@ -237,10 +237,10 @@ Segnala problemi & richiedi funzionalità:\n https://git.chinwag.org/chinwag/chinwag-android/issues Profilo di Tusky - Condividi contenuto del toot - Condividi link al toot - Immagini - Video + Condividi contenuto del toot + Condividi link al toot + Immagini + Video In attesa di approvazione in %d a @@ -290,11 +290,11 @@ Blocca account Richiede la tua approvazione manuale di chi ti segue Salvare bozza? - Inviando il Toot… - Errore durante l\'invio - Invio Toot - Invio annullato - Una copia del toot è stata salvata nelle tue bozze + Inviando il Toot… + Errore durante l\'invio + Invio Toot + Invio annullato + Una copia del toot è stata salvata nelle tue bozze Componi La tua istanza %s non ha nessuna emoji personalizzata Copiato negli appunti @@ -302,8 +302,8 @@ Predefiniti del sistema Dovrai prima scaricare questo pacchetto di emoji Eseguendo una ricerca… - Espandi/Riduci tutti gli stati - Apri toot + Espandi/Riduci tutti gli stati + Apri toot Riavvio dell\'app richiesto Devi riavviare Tusky per applicare queste modifiche Più tardi @@ -345,15 +345,15 @@ limite massimo di %1$d tab raggiunto limite massimo di %1$d tab raggiunto - Media: %s + Media: %s - Contenuto sensibile: %s + Contenuto sensibile: %s - Nessuna descrizione + Nessuna descrizione - Ribloggato + Ribloggato - Apprezzato + Apprezzato Pubblico @@ -374,7 +374,7 @@ Mostra indicatore per bot Sei sicuro di voler permanentemente eliminare tutte le tue notifiche\? Cancella e riscrivi - Cancellare e riscrivere questo toot\? + Cancellare e riscrivere questo toot\? %s voto %s voti @@ -402,7 +402,7 @@ Aggiungi sondaggio Fatto con Tusky Espandi sempre i toot segnalati come contenuto sensibile - Messo nei segalibri + Messo nei segalibri Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s Scegli lista Lista @@ -432,7 +432,7 @@ Altri commenti Inoltra a %s Errore durante l\'invio - Errore durante lo scaricamento degli aggiornamenti + Errore durante lo scaricamento degli aggiornamenti La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè vuoi segnalare questo utente qui sotto: L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\? Utenti @@ -451,9 +451,9 @@ Scelta %d Modifica Errore nella ricerca del post %s - Toot programmati - Toot programmati - Programma un toot + Toot programmati + Toot programmati + Programma un toot RIpristina %1$s • %2$s Non hai bozze. @@ -479,7 +479,7 @@ Mostra le anteprime dei collegamenti nelle timelines Mastodon ha un intervallo minimo di programmazione di 5 minuti. Non ci sono annunci. - Non hai stati pianificati. + Non hai stati pianificati. Abilita il gesto di scorrimento per passare da una scheda all\'altra Notifiche sulle richieste di essere seguiti Parte inferiore @@ -504,13 +504,34 @@ Non puoi caricare più di %1$d allegato multimediale. Non puoi caricare più di %1$d allegati multimediali. - Il toot a cui hai scritto una risposta è stato rimosso + Il toot a cui hai scritto una risposta è stato rimosso Bozza cancellata - L\'invio di questo toot è fallito! + L\'invio di questo toot è fallito! Sei sicuro di voler cancellare la lista %s\? Indefinita Durata - Allegati - Audio + Allegati + Audio Mostra le animazioni delle emojis personalizzate + Iscriviti + Rimuovere questa conversazione\? + Errore nel recuperare le informazioni sulla risposta + Disiscriviti + Rimuovi conversazione + Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include: +\n +\n - Notifiche riguardo a Preferiti/Boost/Following +\n - Conteggio dei Preferiti/Boost nei toot +\n - Statistiche riguardo a Preferiti e Post nei profili +\n +\n Le notifiche push non saranno influenzate, ma puoi rivedere le tue impostazioni delle notifiche manualmente. + Rimuovi segnalibro + Chiedi conferma prima di boostare + 14 giorni + 30 giorni + 60 giorni + 90 giorni + 180 giorni + 365 giorni + Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler controllare queste richieste di following da parte questi account manualmente. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f9d868fe..bb8c892f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -25,9 +25,9 @@ ダイレクトメッセージ タブ スレッド - 投稿 - 投稿と返信 - ピン留め + 投稿 + 投稿と返信 + ピン留め フォロー フォロワー お気に入り @@ -37,14 +37,14 @@ プロフィールを編集 下書き ライセンス - %sさんがブーストしました - 閲覧注意 - 非表示のメディア - タップして表示 - 続きを表示 - 続きを隠す - 続きを読む - 閉じる + %sさんがブーストしました + 閲覧注意 + 非表示のメディア + タップして表示 + 続きを表示 + 続きを隠す + 続きを読む + 閉じる 何もありません。 現在トゥートはありません。更新するにはプルダウンしてください! %sさんがトゥートをブーストしました @@ -111,14 +111,14 @@ リンクをコピー %s として開く 共有先… - トゥートのURLを共有… - トゥートを共有… + トゥートのURLを共有… + トゥートを共有… メディアを共有… 送信しました! ブロックを解除しました ミュートを解除しました - 送信しました! - 返信しました。 + 送信しました! + 返信しました。 インスタンス 今なにしてる? 注意書き @@ -143,7 +143,7 @@ ダウンロード フォローリクエストを取り消しますか? このアカウントをフォロー解除しますか? - 本当に削除しますか? + 本当に削除しますか? 公開:公開タイムラインに投稿する 未収載:公開タイムラインには表示しない フォロワー限定:フォロワーだけに公開 @@ -171,8 +171,8 @@ Chrome Custom Tabsを使用する スクロール中は投稿ボタンを隠す 言語 - タイムラインのフィルタリング - タブ + タイムラインのフィルタリング + タブ ブーストを表示 返信を表示 メディアのプレビューを表示する @@ -188,12 +188,12 @@ 公開 未収載 フォロワーに限定 - トゥートのテキストサイズ - 最小 - - - - 最大 + トゥートのテキストサイズ + 最小 + + + + 最大 新しい返信 新しい返信の通知 新しいフォロワー @@ -228,10 +228,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Tusky公式アカウント - トゥートの内容を共有 - トゥートへのリンクを共有 - 画像 - 動画 + トゥートの内容を共有 + トゥートへのリンクを共有 + 画像 + 動画 フォローリクエスト中 %d年後 @@ -268,11 +268,11 @@ アカウントをロック フォロワーを手動で承認する必要があります 下書きを保存しますか? - トゥートを送信中です… - トゥート送信エラー - トゥートの送信 - 送信がキャンセルされました - トゥートのコピーが下書きに保存されました + トゥートを送信中です… + トゥート送信エラー + トゥートの送信 + 送信がキャンセルされました + トゥートのコピーが下書きに保存されました 新規投稿 インスタンス %s にはカスタム絵文字がありません クリップボードにコピーされました @@ -280,8 +280,8 @@ システムのデフォルト 最初にこれらの絵文字セットをダウンロードする必要があります 検索中… - 全て開く/閉じる - トゥートを開く + 全て開く/閉じる + トゥートを開く アプリの再起動が必要です これらの変更を適用するには、Tuskyの再起動が必要になります 後で @@ -325,7 +325,7 @@ リスト名 ネットワークエラーが発生しました!接続を確認してもう一度試してください! - \@%s + \@%s ブーストを解除 お気に入りを解除 ブーストを表示 @@ -362,7 +362,7 @@ 投票 返信 返信 - このトゥートを削除し、下書きに戻しますか? + このトゥートを削除し、下書きに戻しますか? 削除 更新 リストを作成できませんでした @@ -389,7 +389,7 @@ ボットマークを表示 GIFアバターを動かす 公開タイムライン - 閲覧注意:%s + 閲覧注意:%s 適用 投票終了 アカウント @@ -401,21 +401,21 @@ ブックマーク 編集 ブックマーク - 予約トゥート - 予約トゥート - 予約トゥート + 予約トゥート + 予約トゥート + 予約トゥート フォロワー %1$sさん、%2$sさん フォローリクエスト %sさんのミュートを解除 %sさんがあなたにフォローリクエストしました \@%sさんを通報しました - 予約した投稿はありません。 + 予約した投稿はありません。 下書きはありません。 項目 %d このアカウントは外部のサーバーにあります。匿名化された通報の複製をそちらにも送信しますか? 通報をサーバーのモデレーターに送信します。以下にこのアカウントを通報理由を入力できます: - 投稿を取得できませんでした + 投稿を取得できませんでした 完了 戻る 続ける @@ -424,8 +424,8 @@ リスト ハッシュタグ ハッシュタグを追加 - 説明なし - メディア: %s + 説明なし + メディア: %s %1$sさん、%2$sさんと他%3$d人 %1$sさん フィルターする語句 @@ -465,7 +465,7 @@ %sさんがトゥートしました お知らせ 本当に %s のすべてをブロックするのですか? そのドメインからのコンテンツは、公開タイムラインや通知に表示されなくなります。また、そのドメインのフォロワーは削除されます。 - 音声 + 音声 ドメイン全体を非表示 Tuskyによって提供されています \ No newline at end of file diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index c467920c..64c660ca 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -11,16 +11,16 @@ Ɣef Tabdart Tabdarin - Tijewwiqt-ik aṭas i ɣuzzifet! + Izen-ik·im aṭas i ɣezzif! Agejdan Iccaren - Tijewwiqt - Iznan - S tririyin + Sensla + Iznan + S tririyin Ẓreg amaɣnu-ik - \@%s - Zeṛ ugar - Zeṛ kra kan + \@%s + Zeṛ ugar + Zeṛ kra kan Ulac walu da. %s yeṭṭafar-ik·ikem-id Err @@ -64,29 +64,29 @@ Nɣel aseɣwen Ldi amzun d %s Bḍu amzun d… - Bḍu aseɣwen n tijewwiq s… - Bḍu tijewwiqt d… + Bḍu aseɣwen n tijewwiq s… + Bḍu tijewwiqt d… Anta tummant\? d-acu i gellan d amaynut\? Nadi… Tiririn… Tugna n umaɣnu Sider - Kkes tijewwiqt-a\? + Kkes tijewwiqt-a\? Ẓreg tilɣa Agrudem Aceɛlal Aberkan Tutlayt - Iccaren + Iccaren Apṛuksi %1$s, %2$s, %3$s d %4$d nniḍen %1$s, %2$s, akked %3$s %1$s akked %2$s Tusky %s Amaɣnu n Tusky - Tugniwin - Tibidyutin + Tugniwin + Tibidyutin Yeṭṭafar-ik·ikem-id Kkes Lqem @@ -94,7 +94,7 @@ Rnu yiwen umiḍan amaynut n Maṣṭudun Aru Tummant-ik·im %s ur tesɛi ara imujiyen udmawanen - Ldi tijewwiqt + Ldi tijewwiqt CC-BY 4.0 CC-BY-SA 4.0 Senṭeḍ @@ -113,14 +113,14 @@ Kkes Azen Ẓreg - Yettwanṭḍen + Yettwanṭḍen Rnu amidya Rnu assenqed Ṭef tugna - Timeẓriwt n tijewwaqt - Sɣiwes tijewwaqt-a - Bḍu agbur n tijewwiqt-a - Bḍu aseɣwen ɣer tijewwiqt + Timeẓriwt n yizen + Sɣiwes tijewwaqt-a + Bḍu agbur n tijewwiqt-a + Bḍu aseɣwen ɣer tijewwiqt Rnu amsizdeg Ẓreg amsizdeg Snulfu-d tabdart @@ -168,7 +168,7 @@ %1$s %1$s akked %2$s %1$s, %2$s akked %3$d nniḍen - Aru tijewwiqt + Aru izen %1$s • %2$s %s n wedɣar @@ -213,10 +213,10 @@ Tibdarin Iseɣwan Yettwaceyyeɛ! - Yettwaceyyaɛ! + Yettwaceyyaɛ! Ula d yiwen n ugmuḍ I yimeḍfaṛen kan - Teɣzi n weḍṛis + Teɣzi n weḍṛis Yettwamdemmar s Tusky Asmel Web n usenfaṛ: \n https://chinwag.org @@ -226,7 +226,7 @@ Sekles amzun d arewway\? Ticki Aṛubut - Yettwarna ɣer ticṛad + Yettwarna ɣer ticṛad deg %dsr deg %dtsd deg %dtsn @@ -234,16 +234,16 @@ %d n tasint id yugran %d n tasinin id yugran - Agbur amḥulfu + Agbur amḥulfu Creḍ allal n teywalt amzun d amḥulfu Wennez tikkelt-nniḍen Asali ur yeddi ara. Tuccḍa deg tuzna n tijewwiqt. Adigan Turagin - Yebḍa-t %s - %s yebḍa tijewwiqt-ik·im - %s yerna tijewwiqt-ik·im ɣer ismenyafen-is + Yebḍa-t %s + %s yebḍa izen-ik·im + %s yerna izen-ik·im ɣer ismenyafen-is Tiririt taruradt Bḍu Kkes beṭu @@ -268,9 +268,9 @@ Bren-it ɣer %s Asentel n wesnas - Ulac ɣur-k·m ula d yiwet n tjewwiqt yettwasɣawsen. - Tijewwiqin yettwasɣawsen - Tijewwiqin yettwasɣawsen + Ulac ɣur-k·m ula d yiwet n tjewwiqt yettwasɣawsen. + Tijewwiqin yettwasɣawsen + Tijewwiqin yettwasɣawsen Sken-d beṭuyat Ihacṭagen Isuturen n teḍfeṛt diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f0255d70..e413d1af 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -26,9 +26,9 @@ 다이렉트 메시지 - - 툿과 답장 - 고정된 툿 + + 툿과 답장 + 고정된 툿 팔로우 팔로워 즐겨찾기 @@ -39,15 +39,15 @@ 프로필 편집 임시 저장 라이선스 - \@%s - %s님이 부스트 했습니다 - 민감한 미디어 - 미디어 숨겨짐 - 클릭하여 보기 - 더 보기 - 숨기기 - 더 보기 - 줄이기 + \@%s + %s님이 부스트 했습니다 + 민감한 미디어 + 미디어 숨겨짐 + 클릭하여 보기 + 더 보기 + 숨기기 + 더 보기 + 줄이기 메시지가 없습니다. 게시물이 없습니다. 아래로 당겨서 새로고침하세요! %s님이 부스트 했습니다 @@ -125,15 +125,15 @@ 공유 다운로드 미디어 다운로드 중 - 툿 URL 공유 - 툿 공유 + 툿 URL 공유 + 툿 공유 미디어 공유 신고를 보냈습니다! 차단이 해제됨 뮤트가 해제됨 %s 숨김 해제됨 - 보냈습니다! - 답장을 보냈습니다. + 보냈습니다! + 답장을 보냈습니다. 인스턴스 주소 지금 무엇을 하고 있나요\? 열람 주의 @@ -158,12 +158,12 @@ 다운로드 팔로우 요청을 취소하시겠습니까\? 이 계정을 팔로우 해제하시겠습니까\? - 이 툿을 삭제하시겠습니까\? - 이 툿을 지우고 다시 작성하시겠습니까\? + 이 툿을 삭제하시겠습니까\? + 이 툿을 지우고 다시 작성하시겠습니까\? 정말로 %s 전체를 숨기시겠습니까\? 모든 공개 타임라인과 알림에서 해당 도메인에서 작성된 컨텐츠를 보지 못합니다. 해당 도메인 팔로워와의 관계가 사라집니다. 도메인 전체를 숨기기 공개: 공개 타임라인에 표시 - "비표시: 공개 타임라인에 표시하지 않음 " + 비표시: 공개 타임라인에 표시하지 않음 비공개: 팔로워에게만 공개 다이렉트: 멘션한 사용자에게만 공개 알림 @@ -193,8 +193,8 @@ 언어/Language 봇 식별자 추가 GIF 아바타 애니메이션 보이기 - 타임라인 필터링 - + 타임라인 필터링 + 부스트 보이기 답장 보이기 미디어 미리보기 다운로드 @@ -210,12 +210,12 @@ 공개 타임라인에 비표시 비공개 - 게시물 글자 크기 - 매우 작게 - 작게 - 보통 - 크게 - 매우 크게 + 게시물 글자 크기 + 매우 작게 + 작게 + 보통 + 크게 + 매우 크게 멘션 누군가 나를 멘션할 때 알림 팔로우 @@ -248,10 +248,10 @@ 버그 신고/건의사항: \n https://git.chinwag.org/chinwag/chinwag-android/issues Tusky 공식 계정 - 이 툿의 내용 공유 - 이 툿의 링크 공유 - 사진 - 비디오 + 이 툿의 내용 공유 + 이 툿의 링크 공유 + 사진 + 비디오 팔로우 요청함 %d년 후 @@ -304,11 +304,11 @@ 계정 잠금 팔로워를 수동으로 승인합니다 작성 중인 내용을 저장하시겠습니까\? - 툿을 보내고 있습니다… - 툿을 보낼 수 없습니다 - 툿을 보내고 있습니다 - 보내기 취소됨 - 복사본이 임시 저장에 저장되었습니다 + 툿을 보내고 있습니다… + 툿을 보낼 수 없습니다 + 툿을 보내고 있습니다 + 보내기 취소됨 + 복사본이 임시 저장에 저장되었습니다 글쓰기 이 인스턴스 %s 은(는) 커스텀 이모지가 없습니다. 클립보드에 복사되었습니다 @@ -316,8 +316,8 @@ 시스템 기본 시스템 기본 외의 이모지를 설정하시려면 우선 다운로드해야 합니다 탐색하고 있습니다… - 모두 보이기/줄이기 - 툿 열기 + 모두 보이기/줄이기 + 툿 열기 어플리케이션 재시작 필요 변경 사항을 적용하려면 Tusky를 재시작해야 합니다 다음에 @@ -357,11 +357,11 @@ 최대 탭 수 %1$d에 도달했습니다 - 미디어: %s - 열람주의: %s - 설명 없음 - 부스트함 - 즐겨찾기함 + 미디어: %s + 열람주의: %s + 설명 없음 + 부스트함 + 즐겨찾기함 공개 타임라인에 비표시 비공개 @@ -405,7 +405,7 @@ 부가 설명 %s에 포워딩 신고하지 못했습니다 - 게시물을 불러오지 못했습니다 + 게시물을 불러오지 못했습니다 인스턴스 관리자에게 신고합니다. 이 계정을 신고하려는 이유를 작성하실 수 있습니다: 이 유저는 다른 인스턴스에 속해 있습니다. 그 인스턴스에도 익명으로 신고 내용을 보내시겠습니까\? 알림 필터 보이기 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 86549097..951ed475 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -38,9 +38,9 @@ നേരേയുള്ള സന്ദേശങ്ങൾ ടാബുകൾ ടൂട്ട് - പോസ്റ്റുകൾ - മറുപടികളോടൊപ്പം - പിൻ ചെയ്തത് + പോസ്റ്റുകൾ + മറുപടികളോടൊപ്പം + പിൻ ചെയ്തത് പിന്തുടരലുകൾ പിന്തുടരുന്നവർ നിശ്ശബ്ദരാക്കിയ ഉപയോക്താക്കൾ @@ -61,15 +61,15 @@ പിന്തുടരുന്നവർ പുതിയത് ലഭിക്കാൻ താഴേക്ക് വലിക്കുക ലഭ്യമല്ല - ചുരുക്കുക - വിപുലപ്പെടുത്തുക - അൽപ്പം കാണിക്കൂ - കൂടുതൽ കാണിക്കൂ - തുറന്ന് കാണുവാൻ - മറയ്ക്കപ്പെട്ട മീഡിയ - സെൻസിറ്റീവ് ഉള്ളടക്കം + ചുരുക്കുക + വിപുലപ്പെടുത്തുക + അൽപ്പം കാണിക്കൂ + കൂടുതൽ കാണിക്കൂ + തുറന്ന് കാണുവാൻ + മറയ്ക്കപ്പെട്ട മീഡിയ + സെൻസിറ്റീവ് ഉള്ളടക്കം അനുവാദം - മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് + മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് ബുക് മാർക്ക് പുനഃക്രമീകരിക്കുക ബ്രൗസറിൽ തുറക്കുക @@ -105,16 +105,16 @@ എഴുതുക പിന്തുടാനുള്ള അപേക്ഷകൾ ബൂട്ട്‌സ് കാണിക്കുക - മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് + മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് കരടുകൾ തിരുത്ത് അറിയിപ്പുകൾ - ടാബുകൾ + ടാബുകൾ അറിയിപ്പുകൾ പ്രഖ്യാപനങ്ങൾ പിന്നീട് സംരക്ഷിച്ചു! - %s ബൂസ്റ്റ് ചെയ്തു + %s ബൂസ്റ്റ് ചെയ്തു ഫലങ്ങൾ ഒന്നും ഇല്ല അയച്ചൂ! പങ്കിടുക @@ -130,7 +130,7 @@ നിങ്ങളെ പിന്തുടരുന്നു ഫോട്ടോ എടുക്കുക തിരയുക… - ചിത്രങ്ങൾ + ചിത്രങ്ങൾ കൂടുതൽ ലഭ്യമാക്കുക സൂചനകൾ ബയോ @@ -157,8 +157,8 @@ പുതുക്കുക അവതാർ ലിങ്കുകൾ - \@%s - വിഡിയോ + \@%s + വിഡിയോ സൂചിപ്പിക്കുക നീക്കം ചെയ്യുക \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0b987231..1acee464 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -26,9 +26,9 @@ Directe berichten Tabs Toot - Toots - Met reacties - Vastgezet + Toots + Met reacties + Vastgezet Volgend Volgers Favorieten @@ -38,15 +38,15 @@ Profiel bewerken Concepten Licenties - \@%s - %s boostte - Gevoelige inhoud - Verborgen media - Klik om te bekijken - Meer tonen - Minder tonen - Uitklappen - Inklappen + \@%s + %s boostte + Gevoelige inhoud + Verborgen media + Klik om te bekijken + Meer tonen + Minder tonen + Uitklappen + Inklappen Hier is niets. Niets te zien. Swipe naar beneden om te verversen! %s boostte jouw toot @@ -120,14 +120,14 @@ Link kopiëren Als %s openen Delen als … - Link van de toot delen - Inhoud van de toot delen met… + Link van de toot delen + Inhoud van de toot delen met… Media delen met … Verzonden! Gebruiker is gedeblokkeerd Gebruiker wordt niet langer genegeerd - Verzonden! - Reactie succesvol verzonden. + Verzonden! + Reactie succesvol verzonden. Welke Mastodonserver? Wat wil je kwijt? Tekstwaarschuwing @@ -152,7 +152,7 @@ Downloaden Het volgverzoek intrekken? Dit account ontvolgen? - Deze toot verwijderen? + Deze toot verwijderen? Openbaar: op openbare tijdlijnen tonen Minder openbaar: niet op openbare tijdlijnen tonen Alleen volgers: alleen aan jouw volgers tonen @@ -180,8 +180,8 @@ Aangepaste tabbladen gebruiken Verberg zwevende tootknop tijdens scrollen Taal - Filteren - Tijdlijnen + Filteren + Tijdlijnen Boosts tonen Reacties tonen Voorbeelden van media tonen @@ -197,12 +197,12 @@ Openbaar Minder openbaar Alleen volgers - Tekstgrootte van toots - Kleinst - Klein - Standaard - Groot - Grootst + Tekstgrootte van toots + Kleinst + Klein + Standaard + Groot + Grootst Nieuwe vermeldingen Meldingen over nieuwe vermeldingen Nieuwe volgers @@ -234,10 +234,10 @@ Foutmeldingen & nieuwe functies aanvragen:\n https://git.chinwag.org/chinwag/chinwag-android/issues Tusky\'s profiel - Deel de inhoud van de toot - Deel de link van de toot - Afbeeldingen - Video + Deel de inhoud van de toot + Deel de link van de toot + Afbeeldingen + Video Volgverzoek verzonden over %dj @@ -270,11 +270,11 @@ Account besloten maken Handmatige goedkeuring vereist voor volgers Concept bewaren? - Toot aan het verzenden… - Verzenden van toot mislukt - Toots aan het verzenden - Verzenden geannuleerd - Een kopie van de toot werd opgeslagen als concept + Toot aan het verzenden… + Verzenden van toot mislukt + Toots aan het verzenden + Verzenden geannuleerd + Een kopie van de toot werd opgeslagen als concept Toot schrijven Jouw server %s heeft geen lokale emojis Naar het klembord gekopieerd @@ -282,8 +282,8 @@ Systeemstandaard Je moet eerst deze emoji-sets downloaden Aan het zoeken… - Alle toots in- of uitklappen - Toot openen + Alle toots in- of uitklappen + Toot openen Herstarten app vereist Je moet Tusky herstarten om deze veranderingen te kunnen doorvoeren Later @@ -325,15 +325,15 @@ maximum van %1$d tab bereikt maximum van %1$d tabs bereikt - Media: %s + Media: %s - Inhoudswaarschuwing: %s + Inhoudswaarschuwing: %s - Geen omschrijving + Geen omschrijving - Geboost + Geboost - Aan favorieten toegevoegd + Aan favorieten toegevoegd Openbaar @@ -366,7 +366,7 @@ Naam van lijst Hashtag zonder # Verwijderen en herschrijven - Deze toot verwijderen en herschrijven\? + Deze toot verwijderen en herschrijven\? Leegmaken Filter Toepassen @@ -420,7 +420,7 @@ Extra opmerkingen Verder naar %s Het rapporteren is mislukt - Het ophalen van toots is mislukt + Het ophalen van toots is mislukt Deze rapportage wordt naar jouw servermoderator(en) gestuurd. Je kunt hieronder een uitleg geven over waarom je het account wilt rapporteren: Het account is van een andere server. Wil je ook een geanonimiseerde kopie van de rapportage daarnaartoe sturen\? Meldingenfilter tonen @@ -439,17 +439,17 @@ Bewerken Geluidsbestanden moeten minder dan 40MB zijn. Bladwijzers - Ingeplande toots + Ingeplande toots Bladwijzer Bewerken Bladwijzers Poll toevoegen - Ingeplande toots - Ingeplande toot + Ingeplande toots + Ingeplande toot Herstellen Powered by Tusky Altijd toots met tekstwaarschuwingen uitklappen - Als bladwijzer toegevoegd + Als bladwijzer toegevoegd Kies een lijst Lijst Accounts @@ -457,16 +457,16 @@ Poll Fout tijdens opzoeken toot %s Je hebt nog geen concepten - Je hebt nog geen ingeplande toots + Je hebt nog geen ingeplande toots Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags - Bijlagen + Bijlagen volgverzoek verstuurd Afmelden Abonneren - De toot waarvoor jij een reactie had opgesteld, is verwijderd - Het versturen van deze toot is mislukt! + De toot waarvoor jij een reactie had opgesteld, is verwijderd + Het versturen van deze toot is mislukt! Weet je zeker dat je de lijst %s wilt verwijderen\? Je kan niet meer dan %1$d mediabijlage uploaden. @@ -488,7 +488,7 @@ %s personen Hashtag toevoegen - Geluid + Geluid Meldingen wanneer iemand waar je op bent geabonneerd een nieuwe toot plaatst Nieuwe toots Meldingen over volgverzoeken diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 033f4578..b9203d7a 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -9,26 +9,26 @@ En ukjent autoriseringsfeil oppsto. Autorisasjon ble nektet. Henting av logintoken feilet. - Statusen er for lang! + Innlegget er for langt! Filen må være mindre enn 8MB. Videofiler må være mindre enn 40MB. Den filtypen kan ikke lastes opp. Den filen kunne ikke åpnes. Trenger tillatelse til å lese media. Trenger tillatelse for å lagre media. - Bilder og videoer kan ikke kobles til samme status. + Bilder og videoer kan ikke kobles til samme innlegg. Opplastingen feilet. - En feil oppsto under sending av tootet. + En feil oppsto under sending av innlegget. Hjem Varsler Lokal Forent Direktemeldinger Faner - Toot - Toots - Med svar - Festet + Tråd + Innlegg + Med svar + Festet Følger Følgere Favoritter @@ -38,19 +38,19 @@ Endre profilen din Kladder Lisenser - \@%s - %s boosted - Sensitivt innhold - Media skjult - Klikk for å vise - Vis mer - Vis mindre - Utvid - Kollaps + \@%s + %s boostet + Sensitivt innhold + Media skjult + Klikk for å vise + Vis mer + Vis mindre + Utvid + Kollaps Her er det ingenting. Her er det ingenting. Dra ned for å oppdatere! - %s boostet tootet ditt - %s favoriserte tootet ditt + %s boostet innlegget ditt + %s favoriserte innlegget ditt %s følger deg Rapporter @%s Ytterligere kommentarer\? @@ -102,7 +102,7 @@ Avvis Søk Kladder - Toot-synlighet + Synlighet på innlegg Innholdsadvarsel Emoji-tastatur Legg til fane @@ -122,14 +122,14 @@ Del som … Last ned media Laster ned media - Del toot-URL til… - Del toot til… + Del innlegg-URL til… + Del innlegg til… Del media til… Sendt! Fjernet blokkering av bruker Fjernet demping av bruker - Sendt! - Svaret ble sendt. + Sendt! + Svaret ble sendt. Hvilken instanse\? Hva skjer\? Innholdsadvarsel @@ -154,7 +154,7 @@ Last ned Trekk tilbake følgeforespørselen\? Slutte å følge denne kontoen\? - Slette dette tootet\? + Slette dette tootet\? Offentlig: Vis i offentlig tidslinjer Ikke oppført: Ikke vis i offentlige tidslinjer Bare følgere: Vis bare til følgere @@ -178,8 +178,8 @@ Bruke Chrome tilpassede faner Skjul skriv-knappen under scrolling Språk - Tidslinjefiltrering - Faner + Tidslinjefiltrering + Faner Vis boosts Vis svar Last ned forhåndsvisning av media @@ -191,13 +191,13 @@ Marker alltid media som sensitivt Publiserer (synkronisert med server) Synkronisering av innstillinger feilet - Størrelse på statustekst + Størrelse på statustekst Nye følgere Varsler om nye følgere - Boosts - Varsler når tootene dine blir boostet + Booster + Varsler når innleggene dine blir boostet Favoritter - Varsler når tootene dine blir favorisert + Varsler når innleggene dine blir favorisert %s nevnte deg %1$s, %2$s, %3$s og %4$d anre %1$s, %2$s, og %3$s @@ -212,10 +212,10 @@ Rapporter feil og ønsker om funksjonalitet her: \n https://git.chinwag.org/chinwag/chinwag-android/issues Tuskys Mastodon-profil - Del inneholdet i tootet - Del link til toot - Bilder - Video + Del inneholdet i innlegget + Del link til innlegget + Bilder + Video Forespørsel sendt om %dd om %dh @@ -271,11 +271,11 @@ Lås konto Krever at du manuelt godkjenner nye følgere Lagre kladd\? - Sender Toot… - Det oppsto en feil under sending av tootet - Sender Toots - Sending avbrutt - En kopi av tootet er lagret i kladdene dine + Sender toot… + Det oppsto en feil under sending av tootet + Sender toots + Sending avbrutt + En kopi av tootet er lagret i kladdene dine Skriv Instansen %s har ingen egendefinerte emojis Kopiert til utklippstavlen @@ -283,8 +283,8 @@ Systemstandard Du må laste ned emoji-samlingene før de kan brukes Gjennomfører oppslag… - Utvid/kollaps alle statuser - Åpne toot + Utvid/kollaps alle statuser + Åpne toot Omstart av applikasjonen er påkrevd Du må starte Tusky på nytt for at endringene skal bli aktive Senere @@ -314,8 +314,8 @@ <b>%1$s</b> Favoritter - <b>%s</b> Boost - <b>%s</b> Boosts + %s Boost + %s Booster Boostet av Favorisert av @@ -326,11 +326,11 @@ grensen på %1$d fane er nådd grensen på %1$d faner er nådd - Media: %s - Innholdsadvarsel: %s - Ingen beskrivelse - Reblogged - Favorisert + Media: %s + Innholdsadvarsel: %s + Ingen beskrivelse + Reblogget + Favorisert Offentlig Ikke listet Følgere @@ -340,12 +340,12 @@ Fjern Filter Bruk - Skriv Toot + Skriv innlegg Skriv Vis at konto er en robot Er du sikker på at du vil slette alle varsler\? Slett og skriv på nytt - Vil du slette dette tottet og skrive det på nytt\? + Vil du slette dette tottet og skrive det på nytt\? %1$s • %2$s %s stemme @@ -362,11 +362,11 @@ Offentlig Ikke listet Kun følgere - Minste - Liten - Medium - Stor - Størst + Minste + Liten + Medium + Stor + Størst Avstemminger er avsluttet Avstemminger Varsler om avstemminger som er avsluttet @@ -399,7 +399,7 @@ Flere kommentarer Videresend til %s Klarte ikke å rapportere - Klarte ikke å hente statuser + Klarte ikke å hente innlegg Rapporten vil bli sendt til instansmoderatoren. Under kan du skrive en forklaring på hvorfor du rapporterer denne kontoen: Kontoen tilhører en annen instans. Vil du også sende en anonymisert kopi av rapporten dit\? Skjulte domener @@ -413,7 +413,7 @@ Når nøkkelordet kun inneholder bokstaver og tall, vil det bare brukes dersom det stemmer overens med hele ordet Kontoer Klarte ikke å søke - Ekspander alltid toots markert med innholdsadvarsel + Ekspander alltid innlegg markert med innholdsadvarsel Legg til avstemming Avstemming 5 minutter @@ -427,20 +427,20 @@ Flere valg Valg %d Endre - Planlagte toots + Planlagte toots Rediger - Planlagte toots - Planlegg toot + Planlagte toots + Planlegg toot Tilbakestill Det oppsto en feil under henting av %s Drevet av Tusky Bokmerker Bokmerke Bokmerker - Bokmerke lagt til + Bokmerke lagt til Velg liste Liste - Du har ingen planlagte statuser. + Du har ingen planlagte innlegg. Du har ikke lagret noen kladder. Lydfiler må være mindre enn 40MB. Mastodon har et minimums planleggingsinterval på 5 minutter. @@ -482,14 +482,14 @@ Informasjon som kan påvirke ditt mentale velvære vil bli skjult. Dette inkluderer: \n \n - Varsler om favorisering, boosts og følgere -\n - Antall favoriseringer og boots på toots -\n - Antall følgere og toots på profiler +\n - Antall favoriseringer og boots på innlegg +\n - Antall følgere og innlegg på profiler \n \n Push-varsler vil ikke påvirkes, men du kan se over dine varselinnstillinger manuelt. Velvære - Varsler når noen jeg følger publiserer en ny toot - Nye toots - noen jeg følger publiserer en ny toot + Varsler når noen jeg følger publiserer et nytt innlegg + Nye innlegg + noen jeg følger publiserer et nytt innlegg %s tootet akkurat Du kan ikke laste opp flere enn %1$d mediavedlegg. @@ -498,12 +498,12 @@ Uendelig Varighet Er du sikker på at du vil slette listen %s\? - Vedlegg - Lyd - Tootet du kladdet et svar til har blitt fjernet + Vedlegg + Lyd + Tootet du kladdet et svar til har blitt fjernet Kladd slettet Lasting av svarinformasjon feilet - Sending av toot feilet! + Sending av toot feilet! Animer egendefinerte emojis Avslutt abonnementet Abonner @@ -512,4 +512,11 @@ Slett samtale Slett bokmerke Vis bekreftelsesdialog når favoritt skal legges til + 30 dager + 60 dager + 90 dager + 180 dager + 365 dager + 14 dager + Komponer toot \ No newline at end of file diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 4b6a27ee..c41fe5a2 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -22,8 +22,8 @@ Notificacions Federacion Tut - Pòstes - Amb responsas + Pòstes + Amb responsas Abonament Seguidors Preferits @@ -33,14 +33,14 @@ Modificar lo perfil Borrolhons Licéncias - %s partejat - Contengut sensible - Mèdia amagat - Clicar per mostrar - Mostrar mai - Mostrar mens - Desplegar - Replegar + %s partejat + Contengut sensible + Mèdia amagat + Clicar per mostrar + Mostrar mai + Mostrar mens + Desplegar + Replegar I a pas res aquí. Davalatz per actualizar ! %s a partajat vòstre tut %s a aimat vòstre tut @@ -97,14 +97,14 @@ Clavièr Emoji Telecargament %1$s Copiar lo ligam - Partejar l\'URL del tut amb… - Partejar vòstre tut amb… + Partejar l\'URL del tut amb… + Partejar vòstre tut amb… Partejar l’imatge amb… Enviat ! Utilizaire desblocat Utilizaire sortit del silenci - Enviat ! - Responsa ben enviada. + Enviat ! + Responsa ben enviada. Quina instància ? A de qué pensatz ? Avís de contengut @@ -128,7 +128,7 @@ Telecargar Anullar la demandar d’abonament ? Volètz quitar de seguir aqueste compte ? - Suprimir aqueste tut \? + Suprimir aqueste tut \? Publica : es visibla a la cronologia publica Pas listada :es pas visibla a las cronologias publicas Solament seguidors : pas que visibla pels vòstres seguidors @@ -153,8 +153,8 @@ Navegador Onglets personalizats de Chrome Amagar lo boton de redaccion en desplaçament - Filtre de la cronologia - Onglets + Filtre de la cronologia + Onglets Mostrar los retuts Mostrar las responsas Mostrar los apercebuts @@ -167,12 +167,12 @@ Publica Pas listat Seguidors solament - Talha de text de l\'estatut - Mendre - Pichona - Mejana - Granda - Grandassa + Talha de text de l\'estatut + Mendre + Pichona + Mejana + Granda + Grandassa Mencions nòvas Notificacions de mencions noves Seguidors nòus @@ -207,10 +207,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Perfil de Tusky - Partejar lo contengut del tut - Partejar lo ligam del tut - Imatges - Vidèo + Partejar lo contengut del tut + Partejar lo ligam del tut + Imatges + Vidèo Demanda d’abonament en %d ans @@ -236,11 +236,11 @@ Clavar lo compte Demanda que validetz manualament los seguidors Salvar lo borrolhon ? - Mandadís del tut… - Error en enviar lo tut - Mandadís dels tuts - Mandadís anullat - Una còpia del tut es estat salvat dins los borrolhons + Mandadís del tut… + Error en enviar lo tut + Mandadís dels tuts + Mandadís anullat + Una còpia del tut es estat salvat dins los borrolhons Redactar L’instància %s es pas compatibla amb los emoji personalizats Copiat al quichapapièr @@ -248,8 +248,8 @@ Çò del sistèma D’en primièr vos cal telecargar los emojis seguents Recèrca… - Desplegar/Plegar totes los estatuts - Dobrir lo tut + Desplegar/Plegar totes los estatuts + Dobrir lo tut Reaviada necessària Vos caldrà reaviar Tusky per aplicar aquestes cambiaments Mai tard @@ -276,8 +276,8 @@ Local Messatges dirèctes Onglets - Penjats - \@%s + Penjats + \@%s Pas res aicí. Suprimir lo partatge Suprimir lo favorit @@ -334,7 +334,7 @@ CC-BY-SA 4.0 %1$s Favorit - 3%1$s4 Favorits + %1$s Favorits %s partatge @@ -349,11 +349,11 @@ nombre maximum d’onglet %1$d atengut nombre maximum d’onglets %1$d atengut - Mèdia : %s - Avertiment : %s - Cap de descripcion - Repartajat - Mes en favorit + Mèdia : %s + Avertiment : %s + Cap de descripcion + Repartajat + Mes en favorit Public Pas listada Seguidors @@ -363,7 +363,7 @@ Escriure un tut Redactar Suprimir e reformular - Suprimir e reformular aqueste tut \? + Suprimir e reformular aqueste tut \? Utilizar lo tèma sistèma Netejar Filtrar @@ -410,7 +410,7 @@ Comentaris suplementaris Transferir a %s Fracàs del senhalament - Recuperacion dels estatuts impossibla + Recuperacion dels estatuts impossibla Lo senhalament serà enviat a vòstre moderator de servidor. Podètz fornir una explicacion de perque senhalatz lo compte çai-jos : Aqueste compte es en un autre servidor. Enviar una còpia anonimizada del senhalament \? Domenis resconduts @@ -438,22 +438,22 @@ Opcions multiplas Opcion %d Modificar - Tuts planificats + Tuts planificats Modificar - Tuts planificats - Planificar de tuts + Tuts planificats + Planificar de tuts Escafar Error en cercant la publicacion %s Propulsat per Tusky Marcapaginas Ajustar als marcapaginas Marcapaginas - Ajustat als marcapaginas + Ajustat als marcapaginas Seleccionar la list Lista Los fichièrs àudio devon èsser inferiors a 40 Mo. Avètz pas cap de borrolhon. - Avètz pas cap de tut planificat. + Avètz pas cap de tut planificat. L’interval minimum de planificacion sus Mastodon e de 5 minutas. Demandas d’abonament Etiquetas @@ -469,7 +469,7 @@ Seguir Borrolhon suprimit Fracàs del cargament de las info de responsa - Fracàs de l’enviament ! + Fracàs de l’enviament ! Volètz vertadièrament suprimir la lista %s \? Repassar las notificacions Enregistrat ! @@ -482,8 +482,8 @@ %s personas Apondre hashtag - Pèças juntas - Àudio + Pèças juntas + Àudio Tuts novèls Notificacions de demandas de seguiment Enbàs diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d4239cf5..f2aa559d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -22,8 +22,8 @@ Lokalne Globalne Wątek - Wpisy - Z odpowiedziami + Wpisy + Z odpowiedziami Śledzeni Śledzący Ulubione @@ -33,12 +33,12 @@ Edytuj profil Szkice Licencje - %s podbił - Wrażliwe treści - Ukryto zawartość multimedialną - Naciśnij, aby wyświetlić - Pokaż więcej - Ukryj + %s podbił + Wrażliwe treści + Ukryto zawartość multimedialną + Naciśnij, aby wyświetlić + Pokaż więcej + Ukryj Pusto tutaj. Pociągnij, aby odświeżyć! %s podbił(-a) Twój wpis %s dodał Twój post do ulubionych @@ -95,13 +95,13 @@ Klawiatura emoji Pobieranie %1$s Skopiuj odnośnik - Udostępnij odnośnik do wpisu… - Udostępnij wpis do… + Udostępnij odnośnik do wpisu… + Udostępnij wpis do… Wyślij! Odblokowano użytkownika Cofnięto wyciszenie użytkownika - Wyślij! - Pomyślnie wysłano odpowiedź. + Wyślij! + Pomyślnie wysłano odpowiedź. Jaka instancja? Co Ci chodzi po głowie? Ostrzeżenie o zawartości @@ -150,8 +150,8 @@ Przeglądarka Używaj niestandardowych kart Chrome Ukryj przycisk śledzenia podczas przewijania - Filtrowanie osi czasu - Zakładki + Filtrowanie osi czasu + Zakładki Pokaż podbicia Pokazuj odpowiedzi Pokazuj podgląd zawartości multimedialnej @@ -164,12 +164,12 @@ Publiczne Niewypisane Tylko dla śledzonych - Rozmiar tekstu wpisów - Najmniejszy - Mały - Średni - Duży - Ogromny + Rozmiar tekstu wpisów + Najmniejszy + Mały + Średni + Duży + Ogromny Nowe wspomnienia Powiadomienia o nowych wspomnieniach Nowi śledzący @@ -204,10 +204,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Profil Tusky’ego - Udostępnij zawartość wpisu - Udostępnij link do postu - Obrazy - Wideo + Udostępnij zawartość wpisu + Udostępnij link do postu + Obrazy + Wideo Wysłano prośbę o możliwość śledzenia w %d lata @@ -237,11 +237,11 @@ Zablokuj konto Wymaga od Ciebie ręcznej akceptacji próśb o śledzenie Czy chcesz zapisać szkic? - Wysyłanie wpisu… - Wystąpił błąd podczas wysyłania wpisu - Wysyłanie wpisów - Anulowano wysyłanie - Kopia wpisu została zapisana jako szkic + Wysyłanie wpisu… + Wystąpił błąd podczas wysyłania wpisu + Wysyłanie wpisów + Anulowano wysyłanie + Kopia wpisu została zapisana jako szkic Nowy wpis Twoja instancja %s nie używa żadnych niestandardowych emoji Skopiowano do schowka @@ -249,8 +249,8 @@ Domyślny systemu Musisz najpierw pobrać te zestawy emoji Wyszukiwanie… - Rozwiń/zwiń wszystkie wpisy - Otwórz wpis + Rozwiń/zwiń wszystkie wpisy + Otwórz wpis Wymagane jest ponowne uruchomienie Musisz uruchomić ponownie Tuskyego, aby zastosować zmiany Później @@ -271,9 +271,9 @@ Wystąpił problem z łącznością! Sprawdź swoje połączenie internetowe i spróbuj ponownie! Pliki wideo muszą być mniejsze niż 40MB. Wiadomości bezpośrednie - Przypięte - Rozwiń - Zwiń + Przypięte + Rozwiń + Zwiń Nic tu nie ma. Usuń z ulubionych Usuń i przeredaguj @@ -289,7 +289,7 @@ Udostępnij jako … Zakładki Ukryte domeny - \@%s + \@%s Cofnij podbicie Ukryte domeny Dodaj głosowanie @@ -302,8 +302,8 @@ Pobieranie mediów Wyślij media do… Domena %s nie jest już schowana - Usunąć ten wpis\? - Usunąć i napisać ponownie ten wpis\? + Usunąć ten wpis\? + Usunąć i napisać ponownie ten wpis\? Czy jesteś pewien/pewna że chcesz zablokować wszystko z domeny %s\? Nie będziesz widzieć zawartości z tej domeny w żadnej osi czasu ani w twoich powiadomieniach. Twoi obserwujący z tej domeny nie będą usunięci. Schowaj całą domenę głosowania zostały zakończone @@ -373,11 +373,11 @@ maksymalna liczba zakładek (%1$d) osiągnięta maksymalna liczba zakładek (%1$d) osiągnięta - Media: %s - Ostrzeżenie o zawartości: %s - Brak opisu - Podbity - Polubiony + Media: %s + Ostrzeżenie o zawartości: %s + Brak opisu + Podbity + Polubiony Publiczny Niewidoczne Śledzący @@ -435,7 +435,7 @@ Dodatkowe komentarze Wyślij zgłoszenie do %s Zgłoszenie nie powiodło się - Pobieranie wpisów nie powiodło się + Pobieranie wpisów nie powiodło się Zgłoszenie zostanie przesłąne do moderatora twojego serwera. Możesz podać przyczynę zgłoszenia tego konta poniżej: To konto jest na innym serwerze. Czy przesłać anonimizowaną kopię zgłoszenia na ten serwer\? Konta @@ -453,22 +453,22 @@ Kilka wyborów Opcja %d Edytuj - Zaplanowane wpisy + Zaplanowane wpisy Edytuj - Zaplanowane wpisy - Zaplanuj wpis + Zaplanowane wpisy + Zaplanuj wpis Resetuj Napędzane przez Tusky Błąd przy wyszukiwaniu wpisu %s Zakładki Dodaj do zakładek Zakładki - Dodane do zakładek + Dodane do zakładek Wybierz listę Lista Pliki audio muszą być mniejsze niż 40MB. Nie masz żadnych szkiców. - Nie masz żadnych zaplanowanych wpisów. + Nie masz żadnych zaplanowanych wpisów. Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania. Prośby o możliwość śledzenia Pytaj o potwierdzenie przed podbiciem @@ -500,21 +500,21 @@ Nie możesz przesłać więcej niż %1$d załączników. Nie udało się załadować informacji o odpowiedzi - Przesłanie wpisu nie powiodło się! + Przesłanie wpisu nie powiodło się! Czy na pewno chcesz usunąć listę %s\? Nie ma ogłoszeń. Ogranicz liczbę powiadomień o zmianach na osi czasu Czas trwania Nowe wpisy - Niektóre informacje, które mogą wpływać na Twoj dobrostan psychiczny zostaną ukryte. W ich skład wchodzą: + Niektóre informacje, które mogą wpływać na Twój dobrostan psychiczny zostaną ukryte. W ich skład wchodzą: \n \n - powiadomienia o ulubionych/podbiciach/obserwowaniu -\n - liczba polubień/podbić toota +\n - liczba polubień/podbić wpisu \n - statystyki obserwujących/postów na profilach \n \nNie będzie to miało wpływu na powiadomienia typu push, ale możesz zmienić ustawienia powiadomień ręcznie. Włącz gest przesuwania by przełączać między zakładkami - Załączniki + Załączniki Powiadomienia o prośbach o obserwowanie ktoś kogo zasubskrybowałem/zasubskrybowałam opublikował nowy wpis Wysłano prośbę o obserwowanie @@ -523,7 +523,7 @@ Anuluj subskrypcję Zasubskrybuj Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont. - Wpis dla którego naszkicowałeś/naszkicowałaś odpowiedź został usunięty + Wpis dla którego naszkicowałeś/naszkicowałaś odpowiedź został usunięty Usunięto szkic Ukryj ilościowe statystyki na profilach Ukryj ilościowe statystyki na postach @@ -531,7 +531,7 @@ Zapisano! Twoja prywatna notatka o tym koncie Czas nieokreślony - Dźwięk + Dźwięk Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz Pozycja głównego paska nawigacji Animuj niestandardowe emoji @@ -542,4 +542,11 @@ %s poprosił(a) o możliwość śledzenia Cię Usuń z zakładek Pytaj o potwierdzenie przed dodaniem do ulubionych + 14 dni + 30 dni + 60 dni + 90 dni + 180 dni + 365 dni + Utwórz wpis \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index eee9856a..5f703152 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -25,8 +25,8 @@ Mensagens Diretas Editar abas Conversa - Toots - Com respostas + Toots + Com respostas Segue Seguidores Favoritos @@ -36,12 +36,12 @@ Editar perfil Rascunhos Licenças - %s deu boost - Mídia sensível - Mídia sensível - Toque para ver - Expandir - Ocultar + %s deu boost + Mídia sensível + Mídia sensível + Toque para ver + Expandir + Ocultar Nada aqui. Arraste para atualizar! %s deu boost no teu toot %s favoritou teu toot @@ -109,14 +109,14 @@ Copiar link Abrir como %s Compartilhar como… - Compartilhar link do toot em… - Compartilhar toot em… + Compartilhar link do toot em… + Compartilhar toot em… Compartilhar mídia via… Enviado! Usuário desbloqueado Usuário dessilenciado - Enviado! - Resposta enviada com sucesso. + Enviado! + Resposta enviada com sucesso. Qual instância? No que você está pensando? Aviso de Conteúdo aqui @@ -141,7 +141,7 @@ Baixar Cancelar solicitação para seguir\? Deixar de seguir esta conta? - Excluir este toot? + Excluir este toot? Público: Postar em linhas públicas Não-listado: Não postar em linhas públicas Privado: Postar só para seguidores @@ -168,8 +168,8 @@ Navegador Usar abas do Chrome Ocultar compositor ao rolar a tela - Filtro da linha do tempo - Abas + Filtro da linha do tempo + Abas Mostrar boosts Mostrar respostas Mostrar prévias de mídia @@ -184,12 +184,12 @@ Erro ao sincronizar configurações Público Não-listado - Tamanho da fonte - Menor - Pequeno - Médio - Grande - Maior + Tamanho da fonte + Menor + Pequeno + Médio + Grande + Maior Menções Notificações sobre menções Seguidores @@ -221,10 +221,10 @@ Reporte bugs e solicite funcionalidades: \n https://git.chinwag.org/chinwag/chinwag-android/issues Perfil do Tusky - Compartilhar conteúdo do toot - Compartilhar link do toot - Imagens - Vídeo + Compartilhar conteúdo do toot + Compartilhar link do toot + Imagens + Vídeo Solicitação enviada em %dy @@ -254,11 +254,11 @@ Trancar perfil Requer aprovação manual de seguidores Salvar rascunho? - Enviando toot… - Erro ao enviar toot - Enviando toots - Envio cancelado - Uma cópia do toot foi salva nos seus rascunhos + Enviando toot… + Erro ao enviar toot + Enviando toots + Envio cancelado + Uma cópia do toot foi salva nos seus rascunhos Compor A sua instância %s não possui emojis personalizados Copiado para a área de transferência @@ -266,8 +266,8 @@ Padrão do sistema É necessário baixar estes pacotes de emojis primeiro Carregando… - Expandir/Ocultar todos os toots - Abrir toot + Expandir/Ocultar todos os toots + Abrir toot É necessário reiniciar o aplicativo É necessário reiniciar o aplicativo para aplicar as alterações Depois @@ -292,10 +292,10 @@ Público Privado Ocorreu um erro de conexão! Por favor, verifique sua internet e tente novamente! - Fixado - \@%s - Mostrar mais - Mostrar menos + Fixado + \@%s + Mostrar mais + Mostrar menos Nada aqui. Excluir e rascunhar Preferências da conta @@ -305,7 +305,7 @@ Abrir mídia #%d Baixar mídia Baixando mídia - Excluir e rascunhar este toot\? + Excluir e rascunhar este toot\? enquetes terminarem Filtros Idioma @@ -353,11 +353,11 @@ excedeu o máximo de %1$d aba excedeu o máximo de %1$d abas - Mídia: %s - Aviso de Conteúdo: %s - Sem descrição - Você deu boost - Favoritado + Mídia: %s + Aviso de Conteúdo: %s + Sem descrição + Você deu boost + Favoritado Não-listado Direto Nome da lista @@ -406,7 +406,7 @@ Comentários adicionais Encaminhar para %s Erro ao denunciar - Erro ao carregar toots + Erro ao carregar toots A denúncia será enviada aos moderadores da instância. Explique por que denunciou a conta: A conta está em outra instância. Enviar uma cópia anônima da denúncia para lá\? Instâncias bloqueadas @@ -434,20 +434,20 @@ Múltiplas opções Opção %d Editar - Agendados + Agendados Editar - Agendados - Agendar toot + Agendados + Agendar toot Cancelar Erro ao pesquisar %s Salvos Salvar Salvos Desenvolvido por Tusky - Salvo + Salvo Selecionar lista Lista - Sem toots agendados. + Sem toots agendados. O áudio deve ser menor que 40MB. Sem rascunhos. Mastodon possui um intervalo mínimo de 5 minutos para agendar. @@ -481,8 +481,8 @@ Notificar sobre toots de quem me interessa quem me interessa tootar Erro ao carregar toot para responder - Erro ao enviar o toot! - O toot em que se rascunhou uma resposta foi excluído + Erro ao enviar o toot! + O toot em que se rascunhou uma resposta foi excluído Rascunho excluído Não é possível anexar mais de %1$d arquivo de mídia. @@ -505,8 +505,8 @@ Sem comunicados. Indefinido Duração - Anexos - Áudio + Anexos + Áudio Novos toots %s recém tootou Comunicados @@ -517,4 +517,12 @@ Excluir esta conversa\? Excluir conversa Deseja excluir a lista %s\? + Remover do Salvos + Solicitar confirmação antes de favoritar + 30 dias + 60 dias + 90 dias + 180 dias + 14 dias + 365 dias \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7656877a..184497b8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -26,9 +26,9 @@ Личные сообщения Вкладки Кутеж - Посты - Посты и ответы - Закреплённые + Посты + Посты и ответы + Закреплённые Подписки Подписчики Избранное @@ -38,15 +38,15 @@ Редактировать профиль Черновики Лицензии - \@%s - %s продвинул(а) - Чувствительный контент - Медиа скрыто - Нажмите для просмотра - Показать больше - Показать меньше - Развернуть - Свернуть + \@%s + %s продвинул(а) + Чувствительный контент + Медиа скрыто + Нажмите для просмотра + Показать больше + Показать меньше + Развернуть + Свернуть Ничего нет. Ничего нет. Потяните вниз, чтобы обновить! %s продвинул(а) вашу запись @@ -123,14 +123,14 @@ Поделиться как… Скачать медиафайл Скачивание медиафайла - Поделиться ссылкой на гудок… - Поделиться гудком с… + Поделиться ссылкой на гудок… + Поделиться гудком с… Поделиться медифайлом… Отправить! Пользователь разблокирован Пользователь включен - Отправлено! - Ответ успешно отправлен. + Отправлено! + Ответ успешно отправлен. Какой узел? Что тут происходит\? Предупреждение о содержании @@ -155,8 +155,8 @@ Скачать Отозвать запрос на подписку\? Отписаться от этого аккаунта? - Удалить гудок\? - Удалить гудок и превратить его в черновик\? + Удалить гудок\? + Удалить гудок и превратить его в черновик\? Публичный: Показать в публичных лентах Неизвестно: Не показывать в публичных лентах Только подписчики: Показать только подписчикам @@ -185,8 +185,8 @@ Используйте пользовательские вкладки Chrome Скрывать кнопку композиционирования гудка при прокрутке ленты Язык - Фильтрование ленты - Вкладки + Фильтрование ленты + Вкладки Показывать продвижения Показывать ответы Загрузить предпросмотр медиаконтента @@ -202,12 +202,12 @@ Публичные Неизвестно Для подписчиков - Размер текста статусов - Крохотный - Маленький - Средний - Большой - Огромный + Размер текста статусов + Крохотный + Маленький + Средний + Большой + Огромный Новые Упоминания Уведомлять о новых упоминаниях Новые подписчики @@ -239,10 +239,10 @@ Отчеты об ошибках и ваши пожелания: \n https://git.chinwag.org/chinwag/chinwag-android/issues Профиль Tusky - Поделиться содержанием гудка - Поделиться ссылкой на гудок - Изображения - Видео + Поделиться содержанием гудка + Поделиться ссылкой на гудок + Изображения + Видео Запрошенные подписки через %dг @@ -317,11 +317,11 @@ Закрыть аккаунт Вам придётся вручную подтверждать подписчиков Сохранить черновик? - Отправка гудка… - Ошибка при отправке гудка - Отправка гудков - Отправка отменена - Копия поста сохранена в ваши черновики + Отправка гудка… + Ошибка при отправке гудка + Отправка гудков + Отправка отменена + Копия поста сохранена в ваши черновики Сочинить У вашего узла %s нет собственных эмодзи Скопировано в буфер обмена @@ -329,8 +329,8 @@ Системный Сперва эти наборы эмодзи нужно скачать Производится поиск… - Раскрыть/свернуть все статусы - Открыть гудок + Раскрыть/свернуть все статусы + Открыть гудок Необходимо перезапустить приложение Вам нужно перезапустить Tusky для применения изменений Позже @@ -378,15 +378,15 @@ достигнут лимит в %1$d вкладок достигнут лимит в %1$d вкладок - + Медиафайл: %s - Предупреждение о содержании: %s - + Предупреждение о содержании: %s + Без описания - Реблогнуто - Понравилось + Реблогнуто + Понравилось Публичный @@ -436,7 +436,7 @@ Жалоба на @%s отправлена Переслать в %s Не удалось пожаловаться - Не удалось получить статусы + Не удалось получить статусы Жалоба будет отправлена модератору вашего узла. Ниже вы можете добавить пояснение о причинах жалобы: Этот аккаунт расположен на другом узле. Отправить анонимную копию жалобы туда\? Показать фильтр уведомлений @@ -456,22 +456,22 @@ Множественный выбор Вариант %d Изменить - Запланированные гудки + Запланированные гудки Редактировать - Запланированные гудки - Запланировать гудок + Запланированные гудки + Запланировать гудок Сброс Закладки Добавить в закладки Закладки Работает на Tusky - Добавлено в закладки + Добавлено в закладки Выбрать список Список Аудиофайлы должны быть меньше 40МБ. Ошибка поиска поста %s У вас нет черновиков. - У вас нет запланированный статусов. + У вас нет запланированный статусов. Минимальный интервал планирования в Mastodon составляет 5 минут. Показывать диалог подтверждения перед продвижением Показывать предпросмотр ссылок в лентах @@ -516,8 +516,8 @@ Самочувствие Неопределённая Продолжительность - Вложения - Аудио + Вложения + Аудио %s только что опубликовал(а) Несмотря на то, что ваша учетная запись не закрыта, наши сотрудники %1$s решили, что вы, возможно, захотите просмотреть запросы на отслеживание от этих учетных записей вручную. @@ -529,10 +529,10 @@ Скрыть количественную статистику по постам Отписаться Подписаться - Гудок который Вы записали в черновик был удален + Гудок который Вы записали в черновик был удален Не удалось загрузить информацию об ответе Черновик удалён - Этот гудок не удалось отправить! + Этот гудок не удалось отправить! Вы действительно хотите удалить список %s\? Скрыть количественную статистику по профилям пользователей Ограничение уведомлений в ленте diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index ffb8a374..bb411871 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -25,17 +25,17 @@ दोषो जातः । न किमप्यत्र । नवीकरणार्थमाकृष्यतामधः ! न किमप्यत्र । - संनिपत्यताम् - विस्तार्यताम् - स्वल्पं दृश्यताम् - अधिकं दृश्यताम् - द्रष्टुमत्र नुद्यताम् - प्रच्छन्नसामग्र्यः - संवेदनशीलो विषयः - %s अप्रकाशयत् - \@%s + संनिपत्यताम् + विस्तार्यताम् + स्वल्पं दृश्यताम् + अधिकं दृश्यताम् + द्रष्टुमत्र नुद्यताम् + प्रच्छन्नसामग्र्यः + संवेदनशीलो विषयः + %s अप्रकाशयत् + \@%s अनुज्ञापत्राणि - कालबद्धदौत्यानि + कालबद्धदौत्यानि लेखविकर्षाः स्वीयव्यक्तिविवरणं सम्पाद्यताम् अनुसरणार्थमनुरोधाः @@ -46,9 +46,9 @@ प्रियाः अनुसर्तारः अनुसरति - कीलिताः - सप्रत्युत्तरम् - प्रकटनानि + कीलिताः + सप्रत्युत्तरम् + प्रकटनानि दौत्यम् पीठिकाः प्रत्यक्षसन्देशाः @@ -106,8 +106,8 @@ चित्रं गृह्यताम् मतदानं युज्यताम् सामग्र्यस्मै विभाज्यताम् … - दौत्यमस्मै विभाज्यताम् … - दौत्यजालस्थलमस्मै विभाज्यताम् … + दौत्यमस्मै विभाज्यताम् … + दौत्यजालस्थलमस्मै विभाज्यताम् … सामग्री अवारोप्यमाणा सामग्री अवारोप्यताम् एवं विभाज्यताम् … @@ -126,11 +126,11 @@ जालस्थलानि पीठिका युज्यताम् पुनरारम्भः - कालबद्धदौत्यं क्रियताम् + कालबद्धदौत्यं क्रियताम् भावचिह्नटङ्कणफलकम् विषयप्रत्यादेशः दौत्यसुदर्शता - कालबद्धदौत्यानि + कालबद्धदौत्यानि लेखविकर्षाः अन्विष्यताम् अस्वीक्रियताम् @@ -161,8 +161,8 @@ विषयप्रत्यादेशः किं वर्तमानमस्ति \? किं विशिष्टस्थलम् \? - सफलं प्रत्युत्तरप्रेषणम् । - प्रेषितम्! + सफलं प्रत्युत्तरप्रेषणम् । + प्रेषितम्! %s अनावृतः सूच्यतां मे यदा ज्योत्या सूच्यताम् @@ -180,8 +180,8 @@ किल अवरुध्यताम् @%s\? प्रदेशः छाद्यताम् निश्चियेन सर्वमेव निषिद्धं भवेदेतस्य जनस्य %s \? कोऽपि विषयो न द्रष्टुं शक्यते तत्प्रदेशात् कस्यामपि समयतालिकायामुत वा ते सूचनापेटिकायाम् । भवदनुसर्तारः तस्मात्प्रदेशान्निष्क्रियन्ते । - विनश्य पुनः लिख्यताम् \? - दौत्यमेतन्नश्यताम्\? + विनश्य पुनः लिख्यताम् \? + दौत्यमेतन्नश्यताम्\? अनुसरणं नश्यताम् \? अवारोप्यताम् उपारोप्यमाणम्… @@ -233,10 +233,10 @@ %dd दि %dy वर्ष अनुसरणं निवेदितम् - चलचित्राणि - चित्राणि - दौत्याय जालस्थानं विभाज्यताम् - दौत्यविषयो विभाज्यताम् + चलचित्राणि + चित्राणि + दौत्याय जालस्थानं विभाज्यताम् + दौत्यविषयो विभाज्यताम् टस्कीवर्यस्य व्यक्तिगतविवरणम् अशुद्धीनामावेदनं वैशिष्ट्यनिवेदनञ्च \n https://git.chinwag.org/chinwag/chinwag-android/issues @@ -266,12 +266,12 @@ नवानुसर्तृृन्नधिकृत्य सूचनाः नवानुसर्तारः नवोल्लेखाः - स्थूलतमः - स्थूलः - मध्यमः - सूक्ष्मः - सूक्ष्मतमः - दौत्यस्य / स्थितेरक्षराकारः + स्थूलतमः + स्थूलः + मध्यमः + सूक्ष्मः + सूक्ष्मतमः + दौत्यस्य / स्थितेरक्षराकारः केवलमनुसर्तृभ्यः अनिर्दिष्टम् सार्वजनिकम् @@ -290,8 +290,8 @@ सामग्रीणां पूर्वोद्घाटनमवारोप्यताम् प्रत्युत्तराणि दृश्यन्ताम् प्रकाशनानि दृश्यन्ताम् - पीठिकाः - समयतालिका-शोधनम् + पीठिकाः + समयतालिका-शोधनम् छादितसामग्रीभ्यो बहुवर्णयुतचित्रं दर्शयतु सञ्जीवितावतारः क्रियताम् स्वचालितयन्त्रेभ्यः सूचको दृश्यताम् @@ -320,11 +320,11 @@ अंशफलकेऽनुसृतम् भवदीयं विशिष्टस्थलं %s स्वीयानुकूलभावचिह्नरहितं वर्तते लिख्यताम् - दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता - प्रेषणं निराकृतम् - प्रेष्यमाणानि - दौत्यप्रेषणे दोषः - दौत्यं प्रेष्यमाणम्… + दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता + प्रेषणं निराकृतम् + प्रेष्यमाणानि + दौत्यप्रेषणे दोषः + दौत्यं प्रेष्यमाणम्… रक्षणीयम् \? स्वयमेवाऽनुसर्तॄणां कृतेऽनुमतिर्दातव्या लेखा अवरुध्यताम् @@ -348,8 +348,8 @@ पश्चात् पुनश्च टस्कीप्रारम्भोऽपेक्षितो वर्तते परिवर्तनानुसरेण चलितुम् अनुप्रयोगप्रारम्भः आवश्यकः - दौत्यमुद्घाट्यताम् - विस्तार्यन्तां नश्यन्तां वा स्थतयः + दौत्यमुद्घाट्यताम् + विस्तार्यन्तां नश्यन्तां वा स्थतयः अन्वेषणं भवद्वर्तते… कीलयतु कीलनं नश्यताम् @@ -372,7 +372,7 @@ प्रकाशनात् प्राक् पुष्टिसंवादमञ्जूषा दर्शनीया जालस्थानप्रदर्शनं समयतालिकायां दर्शयतु मास्टोडोने पञ्चनिमेषपरिमितो न्यूनतमः कालबद्धसमयः । - न ते कालबद्धदौत्यानि सन्ति । + न ते कालबद्धदौत्यानि सन्ति । सम्पाद्यताम् मतम् %d बहूनि मतानि @@ -391,7 +391,7 @@ व्यक्तित्वविवरणलेखाः अन्यजालवितारकादियं व्यक्तित्वविवरणलेखा । आवेदनस्य रक्षितप्रतिलिपिरपि प्रेष्यतां वा \? आवेदनमिदं जालवितारकाय प्रेष्यते । स्वीयविवरणमस्मिन् विषयेऽधो लेखितुं शक्नोषि-: - दौत्यानि गृहीतुं विफलता + दौत्यानि गृहीतुं विफलता आवेदनप्रेषणे विफलता अस्मै पुरस्क्रियताम् %s अन्याः टिप्पण्यः @@ -447,12 +447,12 @@ अनुसर्तारः अनिर्दिष्टम् सार्वजनिकम् - पुटचिह्नं कृतम् - प्रीतिर्दत्ता - पुनर्लिखितम् - विवरणं नास्ति - विषयपूर्वसतर्कता: %s - सामग्र्यः %s + पुटचिह्नं कृतम् + प्रीतिर्दत्ता + पुनर्लिखितम् + विवरणं नास्ति + विषयपूर्वसतर्कता: %s + सामग्र्यः %s अधिकतमपीठिकासङ्ख्या %1$d भूता diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 5b88bf6f..df654a29 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -14,7 +14,7 @@ දවස් 1 දවස් 7 සංස්කරණය - පිළිතුරු සමඟ + පිළිතුරු සමඟ අවහිර කළ පරිශීලකයින් කටුපිටපත් කිසිවක් නැත. නැවුම් කිරීමට පහළට අදින්න! @@ -28,26 +28,26 @@ සම්බන්ධ වෙමින්… දැනුම්දීම් ප්‍රසිද්ධ - ඇමුණුම් + ඇමුණුම් නව සැඳහුම් රචනා කරන්න - තව පෙන්වන්න - ටූට් හි පිටපතක් ඔබගේ කටුපිටපත් තුළට සුරකින ලදි + තව පෙන්වන්න + ටූට් හි පිටපතක් ඔබගේ කටුපිටපත් තුළට සුරකින ලදි මාධ්‍ය සඟවන්න පැතිකඩ සංස්කරණය යැවිණි! යළි සකසන්න - අඩුවෙන් පෙන්වන්න + අඩුවෙන් පෙන්වන්න තත්. %d කින් නිහඬ කළ පරිශීලකයින් බෙදාගන්න %1$s ගෙන ගොස් ඇත: - ටූට් වෙත සබැඳියක් බෙදාගන්න + ටූට් වෙත සබැඳියක් බෙදාගන්න බලපත්‍ර පැතිකඩ සංස්කරණය දර්ශන නාමය - මධ්‍යම - ශ්‍රව්‍ය + මධ්‍යම + ශ්‍රව්‍ය දැනුම්දීම් පෙරහන පෙන්වන්න දව. %d නික්මෙන්න @@ -58,8 +58,8 @@ %1$s • %2$s යළි උත්සාහය ගිණුම අගුළුලන්න - මාධ්‍ය සැඟවී ඇත - පිළිතුර සාර්ථකව යැවිණි. + මාධ්‍ය සැඟවී ඇත + පිළිතුර සාර්ථකව යැවිණි. ඒකාබද්ධ උඩුගත වෙමින්… ගිණුම එකතු කරන්න @@ -73,10 +73,10 @@ \@%s වාර්තා කරන්න අතිරේක අදහස්\? සඳහන් කළ - වෙත ටූට් ඒ.ස.නි. බෙදාගන්න… + වෙත ටූට් ඒ.ස.නි. බෙදාගන්න… දීප්ත කටුපිටපත සුරකින්නද\? - සංවේදී අන්තර්ගතයකි + සංවේදී අන්තර්ගතයකි ස්වයංක්‍රමලේඛය පොත්යොමුව ගූගල් හි වත්මන් ඉමෝජි කට්ටලය @@ -118,18 +118,18 @@ දැනුම්දීම් පරිශීලක අනවහිර කෙරිණි දත්ත එක්කරන්න - වෙත ටූට් බෙදාගන්න… - යැවීම අවලංගු කෙරිණි + වෙත ටූට් බෙදාගන්න… + යැවීම අවලංගු කෙරිණි අනවහිර ටූට්! ප්‍රියතමයන් %1$s බාගත වෙමින් - තත්ව ගෙන ඒමට අසමත් විය - මාධ්‍ය: %s + තත්ව ගෙන ඒමට අසමත් විය + මාධ්‍ය: %s සංවාදය නිහඬ කරන්න - ටූට්ස් යැවෙමින් + ටූට්ස් යැවෙමින් සංවාද - විශාල + විශාල ප්‍රසිද්ධ නව මාස්ටඩන් ගිණුමක් එක්කරන්න මාධ්‍ය උඩුගත වීම අහවර වෙමින් @@ -142,7 +142,7 @@ ප්‍රියතමයන් පිළිතුරු… කාලරේඛා දැනුම්දීම් සීමාකරන්න - ටූට් යැවීමේ දෝෂයකි + ටූට් යැවීමේ දෝෂයකි පෙරහන එකතු කරන්න සැමවිටම මාධ්‍ය සංවේදී ලෙස සලකුණු කරන්න යෙදුම යළි ඇරඹීම ඇවැසිය @@ -152,7 +152,7 @@ මෙම වෙනස්කම් යෙදීමට ඔබ ටුස්කි නැවත ඇරඹිය යුතුය සංස්කරණය ඉදිරියට - කාලරේඛාව පෙරීම + කාලරේඛාව පෙරීම %1$s, %2$s සහ තවත් %3$d කාලරේඛා %s නිහඬ කරන්න @@ -161,10 +161,10 @@ අවහිර මාධ්‍ය බාගන්න සැඟවුනු වසම් - කුඩා + කුඩා අන්තර්ගත අවවාද සමඟ ඇති ටූට්ස් සැමවිටම විහිදන්න පෙරසේ - විහිදන්න + විහිදන්න නිහඬ කරන්න ටුස්කි %s වියමන අඩවිය: @@ -177,7 +177,7 @@ මාධ්‍ය එකතු කරන්න ද. %d කින් %1$s, %2$s, %3$s සහ වෙනත් %4$d - ටූට් විවෘත කරන්න + ටූට් විවෘත කරන්න සැඟවුනු වසම් පැය %d සැකසුම් සමමුහූර්ත වීමට අසමත් විය @@ -202,7 +202,7 @@ භාෂාව සුරැකිණි! ටුස්කි\'හි පැතිකඩ - ටූට්හි අන්තර්ගතය බෙදාගන්න + ටූට්හි අන්තර්ගතය බෙදාගන්න අන්තර්ගතය නව ටූට්ස් #%d මාධ්‍ය විවෘත කරන්න @@ -213,9 +213,9 @@ වෙත මාධ්‍ය බෙදාගන්න… ප්‍රසිද්ධ කාලරේඛා - කුඩාම + කුඩාම මත විමසුම් - දැකීමට ඔබන්න + දැකීමට ඔබන්න වසා ඇත දවස් %d ක් ඉතිරිය @@ -248,23 +248,23 @@ මෙම ගිණුම පිළිබඳව ඔබගේ පෞද්ගලික සටහන වසන්න දැනුම්දීම් - \@%s - යැවිණි! + \@%s + යැවිණි! මාස්ටඩන් සමඟ පිවිසෙන්න ජන්දය මුළු වචනය - මෙම ටූට් යැවීමට අසමත් විය! - දෘශ්‍යකය + මෙම ටූට් යැවීමට අසමත් විය! + දෘශ්‍යකය පසුව සංස්කරණය අඳුරු කිසිවක් නැත. - ටූට් යැවෙමින්… + ටූට් යැවෙමින්… පද්ධති පෙරනිමිය සඳැහුම ඉවත් කරන්න දැනුම්දීම් සඟවන්න - තත්ව පාඨයේ ප්‍රමාණය + තත්ව පාඨයේ ප්‍රමාණය කාලරේඛාවෙහි සබැඳි පෙරදසුන් පෙන්වන්න සබැඳි අතිරික්සුව diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 5b026442..15d05945 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -12,18 +12,18 @@ Vyskytla sa chyba. Oznámenia Priame správy - S odpoveďmi - Pripnuté + S odpoveďmi + Pripnuté Sledujúci Záložky Licencie - \@%s - Citlivý obsah - Kliknite pre zobrazenie - Zobraziť viac - Zobraziť menej - Rozbaliť - Zabaliť + \@%s + Citlivý obsah + Kliknite pre zobrazenie + Zobraziť viac + Zobraziť menej + Rozbaliť + Zabaliť Nahlásiť používateľa @%s Rýchla odpoveď Odpovedať @@ -63,8 +63,8 @@ Otvoriť ako %s Odoslané! Používateľ bol odblokovaný - Odoslané! - Odpoveď bola úspešne odoslaná. + Odoslané! + Odpoveď bola úspešne odoslaná. Ktorý server\? Zobrazované meno O vás @@ -85,24 +85,24 @@ TOOT TOOT! Viditeľnosť tootu - Naplánovať toot - Vymazať tento toot\? - Vymazať a prepísať tento toot\? - Zdieľať obsah tootu - Zdieľať odkaz tootu - Odosielanie tootu… - Chyba pri odosielaní tootu - Kópia vášho tootu bola uložená do konceptov - Otvoriť toot + Naplánovať toot + Vymazať tento toot\? + Vymazať a prepísať tento toot\? + Zdieľať obsah tootu + Zdieľať odkaz tootu + Odosielanie tootu… + Chyba pri odosielaní tootu + Kópia vášho tootu bola uložená do konceptov + Otvoriť toot Napísať toot - Plánované tooty - Plánované tooty + Plánované tooty + Plánované tooty Avatar Odstrániť Uzamknúť účet Uložiť koncept\? - Odosielanie tootov - Odosielanie bolo zrušené + Odosielanie tootov + Odosielanie bolo zrušené Vyhľadávanie… Neskôr Reštartovať @@ -114,7 +114,7 @@ Obsah %1$s %1$s a %2$s - Žiadny popis + Žiadny popis Verejný Podporiť Prestať podporovať @@ -131,5 +131,5 @@ Obľúbené Odstrániť Obľúbené - Panely + Panely \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index c5b531b7..4919b3ec 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -24,9 +24,9 @@ Neposredna Sporočila Zavihki Tut - Objave - Z odgovori - Pripeto + Objave + Z odgovori + Pripeto Sledi Sledilci Priljubljene @@ -36,18 +36,18 @@ Uredi svoj profil Osnutki Licence - \@%s - Občutljiva vsebina - Medij je skrit - Kliknite za ogled - Pokaži več - Pokaži manj - Razširi - Strni + \@%s + Občutljiva vsebina + Medij je skrit + Kliknite za ogled + Pokaži več + Pokaži manj + Razširi + Strni Tukaj ni ničesar. Tukaj ni ničesar. Potegnite navzdol za osvežitev! %s je spodbudil tvoj tut - % je vzljubil vaš tut + %s je vzljubil vaš tut %s vam sledi Prijavi @%s Dodatni komentarji\? @@ -115,12 +115,12 @@ Deli kot … Prenos medija Prejemanje medija - Deli URL tuta z… - Deli tut z… + Deli URL tuta z… + Deli tut z… Deli medij z… Pošlji! - Pošlji! - Odgovor je bil uspešno poslan. + Pošlji! + Odgovor je bil uspešno poslan. Kaj se dogaja\? Opozorilo o vsebini Prikazano ime @@ -153,7 +153,7 @@ Prejmi Želite preklicati to zahtevo\? Prenehajte slediti temu računu\? - Želite izbrisati ta tut\? + Želite izbrisati ta tut\? Javno: Objavi v javnih časovnicah Ni prikazano: Ne prikaže v javnih časovnicah Samo sledilci: Objavi samo sledilcem @@ -177,8 +177,8 @@ Uporabi Chromove zavihke po meri Med pomikanjem skrij gumb za sestavljanje Jezik - Filtriranje časovnice - Zavihki + Filtriranje časovnice + Zavihki Pokaži spodbude Pokaži odgovore Prenesi predoglede medijev @@ -191,7 +191,7 @@ Vedno označite medije kot občutljive Objavljanje (sinhronizirano s strežnikom) Nastavitev ni bilo mogoče sinhronizirati - Velikost besedila statusa + Velikost besedila statusa Nove omembe Obvestila o novih omembah Novi sledilci @@ -216,10 +216,10 @@ Poročila o napakah in želje za nove funkcije: \nhttps://git.chinwag.org/chinwag/chinwag-android/issues Profil Tusky - Deli vsebino tuta - Deli povezavo do tuta - Slike - Video + Deli vsebino tuta + Deli povezavo do tuta + Slike + Video Prošnja za sledenje v %dy v %dd @@ -269,11 +269,11 @@ Zakleni račun Zahtevana je ročna potrditev sledilcev Shrani osnutek\? - Pošiljanje tuta… - napaka pri pošiljanju tuta - Pošiljanje tutov - Pošiljanje je preklicano - Kopija tuta je bila shranjena v osnutke + Pošiljanje tuta… + napaka pri pošiljanju tuta + Pošiljanje tutov + Pošiljanje je preklicano + Kopija tuta je bila shranjena v osnutke Sestavi Vaše vozlišče %s nima emotikonov po meri Kopirano v odložišče @@ -281,13 +281,13 @@ Privzete nastavitve sistema Najprej boste morali prenesti te emotikone Izvajanje iskanja… - Razširi/Strni vse statuse - Odpri tut + Razširi/Strni vse statuse + Odpri tut Potreben je ponovni zagon aplikacije Če želite uveljaviti te spremembe, morate znova zagnati Tusky Kasneje Znova zaženi - "Privzeti komplet emotikonov vaše naprave " + Privzeti komplet emotikonov vaše naprave Blob emotikoni so znani od Android 4.4-7.1 Mastodonov privzeti komplet emotikonov Prenos ni uspel @@ -321,11 +321,11 @@ doseženih maksimalnih %1$d zavihkov - Mediji: %s - Opozorila o vsebini: %s - Brez opisa - Ponovno objavljen - Priljubljene + Mediji: %s + Opozorila o vsebini: %s + Brez opisa + Ponovno objavljen + Priljubljene Javno Ni prikazano Sledilci @@ -340,7 +340,7 @@ Prikaži kazalnik za robote Ali ste prepričani, da želite trajno izbrisati vsa obvestila\? Izbriši in preoblikuj - Izbriši in preoblikuj tut\? + Izbriši in preoblikuj tut\? %1$s • %2$s %s glas @@ -360,11 +360,11 @@ Javno Ni prikazano Samo za sledilce - Najmanjša - Majhna - Srednja - Velika - Največja + Najmanjša + Majhna + Srednja + Velika + Največja Ankete Obvestilo o anketah, ki so se končale Dejanje za sliko %s @@ -404,7 +404,7 @@ Dodatni komentarji Posreduj %s Prijava je bila neuspešna - Statusov ni bilo mogoče pridobiti + Statusov ni bilo mogoče pridobiti Poročilo bo poslano moderatorju strežnika. Spodaj lahko navedete, zakaj prijavljate ta račun: Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik\? Skrite domene @@ -432,14 +432,14 @@ Več izbir Izbira %d Uredi - Napovedani tuti + Napovedani tuti Uredi - Napovedani tuti + Napovedani tuti Ponastavi - Napovej tut + Napovej tut Napaka pri iskanju objave %s Poganja ga Tusky - %s spodbudil + %s spodbudil Ključniki Zahteve za Sledenje diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4e0a6e14..1bbc64a8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -26,9 +26,9 @@ Direkta meddelanden Flikar Toot - Inlägg - Med svar - Fastnålade + Inlägg + Med svar + Fastnålade Följer Följare Favoriter @@ -38,15 +38,15 @@ Ändra din profil Utkast Licenser - \@%s - %s knuffade - Känsligt innehåll - Dold media - Tryck för att visa - Visa mer - Visa mindre - Expandera - Dölj + \@%s + %s knuffade + Känsligt innehåll + Dold media + Tryck för att visa + Visa mer + Visa mindre + Expandera + Dölj Ingenting här. Inget här. Dra ner för att uppdatera! %s knuffade din toot @@ -120,14 +120,14 @@ Kopiera länk Öppna med %s Dela som … - Dela toot-URL till… - Dela toot till… + Dela toot-URL till… + Dela toot till… Dela media till… Skickat! Användare avblockerad Användaren är inte tystad längre - Skickat! - Svar skickades framgångsrikt. + Skickat! + Svar skickades framgångsrikt. Vilken instans? Vad händer? Innehållsvarning @@ -153,7 +153,7 @@ Ladda ned Återkalla följningsförfrågan? Sluta följ detta konto\? - Radera denna toot? + Radera denna toot? Offentlig: Skicka till offentliga tidslinjer Olistad: Visa inte i offentliga tidslinjer Enbart-följare: Ses enbart av följare @@ -182,8 +182,8 @@ Använd Chrome-anpassade flikar Dölj skriv-knappen vid skrollning Språk - Filtrering av tidslinje - Flikar + Filtrering av tidslinje + Flikar Visa knuffar Visa svar Visa en förhandsgranskning @@ -199,12 +199,12 @@ Offentlig Olistad Endast följare - Textstorlek på status - Minsta - Liten - Mellan - Stor - Största + Textstorlek på status + Minsta + Liten + Mellan + Stor + Största Nya omnämnanden Aviseringar om nya omnämnanden Nya följare @@ -237,10 +237,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Tuskys Profil - Dela innehåll av toot - Dela länk till toot - Bilder - Video + Dela innehåll av toot + Dela länk till toot + Bilder + Video Följarförfrågan om %dy @@ -290,11 +290,11 @@ Lås konto Kräver att du manuellt godkänner följare Spara utkast? - Skickar toot… - Kunde inte skicka toot - Skickar toot - Sändning avbruten - En kopia av tooten har sparats i dina utkast + Skickar toot… + Kunde inte skicka toot + Skickar toot + Sändning avbruten + En kopia av tooten har sparats i dina utkast Skriv Din instans %s har inga anpassade emojis Kopierat till urklipp @@ -302,8 +302,8 @@ Systemstandard Du behöver ladda ned dessa emojis först Utför sökning… - Expandera/Dölj alla statusar - Öppna toot + Expandera/Dölj alla statusar + Öppna toot Omstart av appen krävs Du måste starta om Tusky för att tillämpa ändringarna Senare @@ -344,14 +344,14 @@ max antal flikar %1$d uppnådd - Media: %s + Media: %s - Innehållsvarning: %s - Ingen beskrivning + Innehållsvarning: %s + Ingen beskrivning - Knuffad + Knuffad - Favoriserad + Favoriserad Publik @@ -373,7 +373,7 @@ Visa robotindikator Är du säker på att du vill rensa dina aviseringar permanent\? Radera och skriv på nytt - Radera och skriv ny toot\? + Radera och skriv ny toot\? %1$s • %2$s %s röst @@ -420,7 +420,7 @@ Ytterligare kommentarer Vidarebefordra till %s Misslyckades att anmäla - Misslyckades att hämta status + Misslyckades att hämta status Anmälan kommer att skickas till din servermoderator. Du kan beskriva varför du anmäler kontot nedan: Kontot är från en annan server. Skicka en anonym kopia av anmälan dit också\? Visa notifikationsfilter @@ -442,20 +442,20 @@ Flerval Val %d Redigera - Schemalagda toots + Schemalagda toots Ändra - Schemalagda toots - Schemalägg toot + Schemalagda toots + Schemalägg toot Återställ Fel vid uppslagning av status %s Bokmärken Bokmärk Bokmärken Drivs av Tusky - Bokmärkt + Bokmärkt Välj lista Lista - Du har inga schemalagda statusar. + Du har inga schemalagda statusar. Ljudfiler måste vara mindre än 40MB. Du har inga utkast. Mastodon har ett minimalt schemaläggningsintervall på 5 minuter. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 203f33e9..6625a19e 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -20,8 +20,8 @@ அறிவிப்புகள் அருகாமயில் ஒருங்கிணைந்த - பதிவுகள் - பதிலளிக்கபட்டவை + பதிவுகள் + பதிலளிக்கபட்டவை பின்பற்றுகிறீர் பின்பற்றுபவர்கள் விரும்பியவை @@ -30,12 +30,12 @@ பின்பற்ற கோரிக்கை சுயவிவரத்தை திருத்த வரைவுகள் - %s மேலேற்றப்பட்டது - உணர்ச்சிகரமான உள்ளடக்கம் - ஊடகம் மறைக்கப்பட்டது - பார்வையிட சொடுக்கவும் - அதிகமாக்கு - கம்மியாக்கு + %s மேலேற்றப்பட்டது + உணர்ச்சிகரமான உள்ளடக்கம் + ஊடகம் மறைக்கப்பட்டது + பார்வையிட சொடுக்கவும் + அதிகமாக்கு + கம்மியாக்கு இங்கு எதுவுமில்லை. புதுப்பிக்க கீழே இழுக்கவும்! %s தங்களின் toot உயர்த்தப்பட்டுள்ளது %s தங்களின் toot பிடித்தவையானது @@ -90,14 +90,14 @@ Emoji விசைபலகை பதிவிறக்கப்படுகிறது %1$s இணைப்பை நகலெடுக்கவும் - Toot URL-யை பகிர… - Toot உள்ளடக்கத்தை பகிர… + Toot URL-யை பகிர… + Toot உள்ளடக்கத்தை பகிர… Mediaவை பகிர… அனுப்பு! பயனர் முடக்கம் நீக்கப்பட்டது பயனர் ஒலிக்க செய்யபட்டது - அனுப்பு! - வெற்றிகரமாக பதிலளிக்கபட்டது. + அனுப்பு! + வெற்றிகரமாக பதிலளிக்கபட்டது. எந்த instance(களம்)? என்ன நடக்கிறது? உள்ளடக்க எச்சரிக்கை @@ -143,8 +143,8 @@ உலாவி Chrome தனிப்பயன் கீற்றை பயன்படுத்து உருளலின் போது எழுது பொத்தானை மறை - காலவரிசை வடிகட்டல் - கீற்றுகள் + காலவரிசை வடிகட்டல் + கீற்றுகள் மேலேற்றத்தை காண்பி பதில்களைக் காண்பி ஊடக மாதிரிக்காட்சிகளைக் காண்பி @@ -158,7 +158,7 @@ அனைவருக்கும் பட்டியலிடப்படாதவர்களுக்கு பின்பற்றுபவர்களுக்கு மட்டும் - நிலை உரை அளவு + நிலை உரை அளவு புதிய குறிப்புகள் புதிய குறிப்புகள் பற்றிய அறிவிப்புகள் புதிய பின்பற்றுபவர்கள் @@ -191,10 +191,10 @@ https://git.chinwag.org/chinwag/chinwag-android/issues Tusky-ன் கணக்கு - Toot உள்ளடக்கத்தைப் பகிர் - Toot இணைப்பைப் பகிர் - படங்கள் - காணொளி + Toot உள்ளடக்கத்தைப் பகிர் + Toot இணைப்பைப் பகிர் + படங்கள் + காணொளி கோரிக்கையைப் பின்பற்றவும் %dஆ-முன் @@ -224,11 +224,11 @@ கணக்கை முடக்கு நீங்களாக பின்பற்றுபவர்களை அங்கீகரிக்க வரைவை சேமிக்கவா? - Toot அனுப்பபடுகிறது… - Toot அனுப்புவதில் பிழை - Toots அனுப்பபடுகிறது - Toot அனுப்பபல் நீக்கபட்டது - நகலெடுக்கபட்ட toot வரைவில் சேமிக்கபட்டது + Toot அனுப்பபடுகிறது… + Toot அனுப்புவதில் பிழை + Toots அனுப்பபடுகிறது + Toot அனுப்பபல் நீக்கபட்டது + நகலெடுக்கபட்ட toot வரைவில் சேமிக்கபட்டது எழுது தங்கள் %s instance(களம்)-ல் எந்தவொரு custom emojis-ம் இல்லை பிடிப்புப்பலகையில் நகலெடுக்க @@ -236,8 +236,8 @@ அமைப்பின் இயல்புநிலை தாங்கள் முதலில் இந்த Emoji sets-னை பதிவிறக்கவேண்டும் சேயல்பாட்டு தேடல்… - அதிகமாக்கு/கம்மியாக்கு பற்றிய நிலைகள் - Tootயை திற + அதிகமாக்கு/கம்மியாக்கு பற்றிய நிலைகள் + Tootயை திற செயலி மறுதொடக்கம் தேவைபடுகிறது இந்த மாறுதல்கள் செயற்படுத்த செயலியை மறுதொடக்கம் செய்ய வேண்டும் பிறகு @@ -266,7 +266,7 @@ டூத் அனுப்புவதில் பிழை ஏற்பட்டுள்ளது நேரடி தகவல் பட்டைகள் - பொருத்தப்பட்டது + பொருத்தப்பட்டது 1 நாள் 3 நாட்கள் 7 நாட்கள் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index c0b270bb..fa27fbe0 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -15,7 +15,7 @@ บัญชี บัญชีนี้มาจากเซิร์ฟเวอร์อื่น ส่งสำเนารายงานที่ไม่ระบุชื่อไปที่นั่นด้วยหรือไม่\? รายงานจะถูกส่งไปยังผู้ดูแลเซิร์ฟเวอร์ของคุณ สามารถให้คำอธิบายว่าทำไมจึงรายงานบัญชีนี้ด้านล่าง: - ดึงข้อมูลสถานะล้มเหลว + ดึงข้อมูลสถานะล้มเหลว รายงานล้มเหลว ส่งต่อไปยัง %s ความคิดเห็นเพิ่มเติม @@ -65,12 +65,12 @@ ผู้ติดตาม ไม่อยู่ในรายการ สาธารณะ - คั่นหน้า - ชื่นชอบ - ได้ถูกเขียนใหม่ - ไม่มีคำอธิบาย - เตือนเนื้อหา : %s - สื่อ: %s + คั่นหน้า + ชื่นชอบ + ได้ถูกเขียนใหม่ + ไม่มีคำอธิบาย + เตือนเนื้อหา : %s + สื่อ: %s ถึงจำนวนแท็บสูงสุดคือ %1$d แล้ว @@ -110,8 +110,8 @@ ภายหลัง จำเป็นต้องเริ่ม Tusky ใหม่ เพื่อใช้การเปลี่ยนแปลงเหล่านี้ จำเป็นต้องเริ่มแอปใหม่ - เปิด Toot - ขยาย/ย่อทั้งหมด + เปิด Toot + ขยาย/ย่อทั้งหมด กำลังค้นหา… ต้องดาวน์โหลดชุดเอโมจิเหล่านี้ก่อน ค่าปริยายของระบบ @@ -119,11 +119,11 @@ คัดลอกไปยังคลิบบอร์ดแล้ว Instance %s ไม่มีเอโมจิแบบกำหนดเอง เขียน - สำเนา Toot บันทึกเป็นฉบับร่างแล้ว - การส่งถูกยกเลิก - ส่ง Toot - การส่ง Toot เกิดข้อผิดผลาด - กำลังส่ง Toot… + สำเนา Toot บันทึกเป็นฉบับร่างแล้ว + การส่งถูกยกเลิก + ส่ง Toot + การส่ง Toot เกิดข้อผิดผลาด + กำลังส่ง Toot… บันทึกฉบับร่าง\? ต้องอนุมัติผู้ติดตามด้วยตัวเอง ล็อกบัญชี @@ -174,10 +174,10 @@ ใน %d วัน ใน %d ปี กำลังขอติดตาม - วิดีทัศน์ - ภาพ - แบ่งปันลิงก์ Toot - แบ่งปันเนื้อหา Toot + วิดีทัศน์ + ภาพ + แบ่งปันลิงก์ Toot + แบ่งปันเนื้อหา Toot บัญชีทางการของ Tusky รายงานช่องโหว่ และ ขอฟีเจอร์ (ภาษาอังกฤษ): \nhttps://git.chinwag.org/chinwag/chinwag-android/issues @@ -206,12 +206,12 @@ ผู้ติดตามใหม่ การกล่าวถึงใหม่ การแจ้งเตือนเกี่ยวกับการกล่าวถึงใหม่ - ใหญ่มาก - ใหญ่ - กลาง - เล็ก - เล็กมาก - ขนาดอักษร Toot + ใหญ่มาก + ใหญ่ + กลาง + เล็ก + เล็กมาก + ขนาดอักษร Toot เฉพาะผู้ติดตาม ไม่อยู่ในรายการ สาธารณะ @@ -227,8 +227,8 @@ ดาวน์โหลดตัวอย่างสื่อ แสดงการตอบกลับ แสดงบูสต์ - แท็บ - คัดกรองไทม์ไลน์ + แท็บ + คัดกรองไทม์ไลน์ อวตาร GIF เคลื่อนไหวได้ แสดงสัญลักษณ์ว่าเป็นบอต ภาษา @@ -265,8 +265,8 @@ บล็อก @%s\? ซ่อนทั้งโดเมน ต้องการบล็อกทุกอย่างจาก %s \? คุณจะไม่เห็นเนื้อหาจากโดเมนนั้นในไทม์ไลน์สาธารณะหรือในการแจ้งเตือน ผู้ติดตามของคุณจากโดเมนนั้นจะถูกลบออก - ลบ แล้ว ร่าง Toot นี้ใหม่\? - ลบ Toot นี้\? + ลบ แล้ว ร่าง Toot นี้ใหม่\? + ลบ Toot นี้\? เลิกติดตามผู้ใช้นี้\? ยกเลิกคำขอติดตาม\? ดาวน์โหลด @@ -290,15 +290,15 @@ คำเตือนเนื้อหา เกิดอะไรขึ้นเอย\? Instance ไหน\? - ตอบกลับสำเร็จ - ส่งแล้ว! + ตอบกลับสำเร็จ + ส่งแล้ว! เลิกซ่อน %s แล้ว เลิกบล็อกผู้ใช้แล้ว เลิกปิดเสียงผู้ใช้นี้แล้ว ส่งแล้ว! แบ่งปันสื่อไป… - แบ่งปัน Toot ไป… - แชร์ URL Toot ไป… + แบ่งปัน Toot ไป… + แชร์ URL Toot ไป… กำลังดาวน์โหลดสื่อ ดาวน์โหลดสื่อ แบ่งปันโดย… @@ -316,11 +316,11 @@ โต้ตอบ ลิงก์ เพิ่มแท็บ - Toot แบบตั้งเวลา + Toot แบบตั้งเวลา คีย์บอร์ดเอโมจิ เตือนเนื้อหา การมองเห็น Toot - Toot แบบตั้งเวลา + Toot แบบตั้งเวลา ฉบับร่าง ปฏิเสธ ยอมรับ @@ -380,17 +380,17 @@ %s ได้ดันโพสต์ของคุณ ไม่มีอะไรที่นี่ ลากลงเพื่อรีเฟรช! ไม่มีอะไรที่นี่ - ย่อ - ขยาย - แสดงน้อยลง - แสดงเพิ่มเติม - แตะเพื่อดู - ซ่อนสื่ออยู่ - เนื้อหาอ่อนไหว - %s ได้ดัน - \@%s + ย่อ + ขยาย + แสดงน้อยลง + แสดงเพิ่มเติม + แตะเพื่อดู + ซ่อนสื่ออยู่ + เนื้อหาอ่อนไหว + %s ได้ดัน + \@%s สัญญาอนุญาต - โพสต์แบบกำหนดเวลา + โพสต์แบบกำหนดเวลา แก้ไขโปรไฟล์ คำขอติดตาม โดเมนที่ซ่อนไว้ @@ -399,9 +399,9 @@ ที่คั่นหน้า ผู้ติดตาม ติดตาม - ปักหมุด - โพสต์และตอบกลับ - โพสต์ + ปักหมุด + โพสต์และตอบกลับ + โพสต์ โพสต์ แท็บ ข้อความโดยตรง @@ -446,7 +446,7 @@ แสดงข้อความยืนยันก่อนที่จะบูสต์ แสดงตัวอย่างลิงก์ในไทม์ไลน์ Mastodon กำหนดเวลาขั้นต่ำ 5 นาที - ไม่มีสถานะแบบตั้งเวลาใด ๆ + ไม่มีสถานะแบบตั้งเวลาใด ๆ ไม่มีฉบับร่างใด ๆ การค้นหาโพสต์ %s เกิดข้อผิดผลาด แก้ไข @@ -461,7 +461,7 @@ ซ่อนการแจ้งเตือน ปิดเสียงการแจ้งเตือนจาก %s ซ่อนหัวข้อของแถบเครื่องมือด้านบน - ล้มเหลวในการส่งโพสต์นี้! + ล้มเหลวในการส่งโพสต์นี้! ข้อมูลบางอย่างที่อาจส่งผลต่อสุขภาพจิตของคุณจะถูกซ่อนไว้ซึ่งรวมถึง: \n \n- การแจ้งเตือน ชื่นชอบ/ดัน/ติดตาม @@ -477,7 +477,7 @@ สุขภาวะ บันทึกส่วนตัวของคุณเกี่ยวกับบัญชีนี้ แจ้งเตือน เมื่อคนที่คุณติดตาม ได้เผยแพร่โพสต์ใหม่ - โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว + โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว ลบฉบับร่างแล้ว ล้มเหลวในการโหลดข้อมูลตอบกลับ คุณต้องการลบลิสต์ %s ใช่ไหม\? @@ -488,8 +488,8 @@ ไม่มีประกาศ ไม่มีกำหนด ระยะเวลา - ไฟล์แนบ - เสียง + ไฟล์แนบ + เสียง โพสต์ใหม่ %s เพิ่งโพสต์ ประกาศ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d2f9294d..25a5efcb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -26,9 +26,9 @@ Direkt mesajlar Sekmeler Toot - Gönderiler - Yanıtlarıyla - Sabitlenmiş + Gönderiler + Yanıtlarıyla + Sabitlenmiş Takip edilenler Takipçiler Favoriler @@ -38,15 +38,15 @@ Profili düzenle Taslaklar Lisanslar - \@%s - %s yineledi - Hasas medya - Gizlenmiş medya - Görüntülemek için dokunun - Daha fazla göster - Daha az göster - Genişlet - Daralt + \@%s + %s yineledi + Hasas medya + Gizlenmiş medya + Görüntülemek için dokunun + Daha fazla göster + Daha az göster + Genişlet + Daralt Burada hiçbir şey yok. Burada henüz hiç birşey yok. Yenilemek için aşağıya çekin! %s tootunuzu yineledi @@ -108,14 +108,14 @@ Bağlantıyı kopyala Farklı aç %s Olarak paylaş … - Durumun adresini paylaş… - Tootu paylaş… + Durumun adresini paylaş… + Tootu paylaş… Medyayı paylaş… Gönderildi! Kullanıcının engeli kaldırıldı Kullanıcının sesi açıldı - İletildi! - Yanıt başarıyla gönderildi. + İletildi! + Yanıt başarıyla gönderildi. Hangi sunucu\? Neler oluyor? İçerik uyarısı @@ -140,7 +140,7 @@ İndir Takip isteğini iptal et\? Takibi bırakmak istiyor musun\? - Bu durumu silmek istiyor musunuz\? + Bu durumu silmek istiyor musunuz\? Kamu: Herkese açık ve sosyal çizelgelerinde çıkar Listelenmemiş: Genel zaman çizelgelerinde gösterme Özel: Sadece takipçiler ve bahsedilenlere açık @@ -166,8 +166,8 @@ Tarayıcı Chrome Özel Şekmelerini Kullan Kaydırırken takip düğmesi gizlensin - Zaman çizelgesi filtreleme - Sekmeler + Zaman çizelgesi filtreleme + Sekmeler Yükseltilenleri göster Yanıtları göster Medya önizlemelerini indir @@ -183,12 +183,12 @@ Herkese açık Liste dışı Sadece takipçiler - Durum metin boyutu - Çok küçük - Küçük - Orta - Büyük - En büyük + Durum metin boyutu + Çok küçük + Küçük + Orta + Büyük + En büyük Yeni Bahsetmeler Yeni bahsetmeler hakkında bildirim Yeni Takipçiler @@ -219,10 +219,10 @@ & özellik istekleri hata raporları: \n https://git.chinwag.org/chinwag/chinwag-android/issues Tusky\'nin Profili - İletinin içeriğini paylaş - İletinin adresini paylaş - Görseller - Video + İletinin içeriğini paylaş + İletinin adresini paylaş + Görseller + Video Takip edebilme istendi %dy @@ -254,11 +254,11 @@ Hesabı Kilitle Takipçileri elle onaylamanız gerekir Taslaklara kaydedilsin mi\? - Toot gönderiliyor… - Toot gönderilirken hata oluştu - Toot Gönderiliyor - Gönderme iptal edildi - Tootun bir kopyası taslaklara kaydedildi + Toot gönderiliyor… + Toot gönderilirken hata oluştu + Toot Gönderiliyor + Gönderme iptal edildi + Tootun bir kopyası taslaklara kaydedildi Oluştur %s örneğinizin herhangi bir özel ifadesi yok Panoya kopyalandı @@ -266,8 +266,8 @@ Sistem varsayılanı Önce bu ifade paketini indirmeniz gerekecek Araştırılıyor… - Tüm durumları Genişlet/Küçült - Durumu aç + Tüm durumları Genişlet/Küçült + Durumu aç Uygulamayı yeniden başlatmanız lazım Bu değişiklikleri uygulamak için Tusky\'yi yeniden başlatmanız gerekecek Sonra @@ -346,11 +346,11 @@ Listeye hesap ekle Hesabı listeden kaldır Google\'ın mevcut ifade paketi - Medya: %s - İçerik uyarısı: %s - Açıklama yok - Yeniden blogladı - Favorilendi + Medya: %s + İçerik uyarısı: %s + Açıklama yok + Yeniden blogladı + Favorilendi Herkese açık Liste dışı Takipçiler @@ -396,7 +396,7 @@ \@%s başarıyla bildirildi Ek Yorumlar %s adresine ilet - Durumlar getirilemedi + Durumlar getirilemedi Bildirim sunucu yöneticinize gönderilecektir. Bu hesabı neden bildirdiğinizle ilgili açıklama yapabilirsiniz: Hesap başka bir sunucudan. Raporun anonim bir kopyasını da oraya gönderilsin mi\? Bildirim filtresini göster @@ -406,19 +406,19 @@ Bahsedenler #%d medyayı aç Yer imleri - Zamanlanmış tootlar + Zamanlanmış tootlar Yer imi Düzenle Sil ve düzenle Yer imleri Anket ekle - Zamanlanmış tootlar - Tootu zamanla + Zamanlanmış tootlar + Tootu zamanla Sıfırla - Bu durumu silip yeniden düzenlemek istiyor musunuz\? + Bu durumu silip yeniden düzenlemek istiyor musunuz\? Botlar için işaret göster Tusky tarafından desteklenmektedir - Yerimine eklendi + Yerimine eklendi Liste seç Liste Hesaplar @@ -444,7 +444,7 @@ Seçenek %d %s gönderisi aranırken hata oluştu Hiç taslağınız yok. - Zamanlanmış durumunuz yok. + Zamanlanmış durumunuz yok. Kendi kitlenize yükseltin Hashtags\'ler Boost etmeden önce onay iletişim kutusunu göster diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fc73d1b5..0031334b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,22 +1,22 @@ Сталася помилка. - \@%s + \@%s Ліцензії Редагувати профіль Запити на підписку Закладки Підписники Підписки - Прикріплені - З відповідями + Прикріплені + З відповідями Загальні Локальні Приватні повідомлення Сповіщення Головна - Помилка надіслання допису. - Зображення та відео не можуть бути прикріплені до статусу одночасно. + Помилка надсилання допису. + Зображення та відео не можуть бути прикріплені до допису одночасно. Потрібен дозвіл на зберігання медіа. Потрібен дозвіл на читання медіа. Не вдається відкрити цей файл. @@ -24,7 +24,7 @@ Аудіофайли повинні бути менше 40 МБ. Відео повинне бути менше 40 МБ. Файл повинен бути менше 8 МБ. - Статус надто довгий! + Допис задовгий! Не вдалося знайти браузер, який можна використати. Не може бути порожнім. Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову! @@ -45,7 +45,7 @@ Пошук… Про себе Що відбувається\? - Надіслати! + Надіслати! Надіслано! Поділитися як … Відкрити як %s @@ -57,7 +57,7 @@ Згадки Посилання Попередження про вміст - Заплановані дмухи + Заплановані дмухи Чернетки Відхилити Прийняти @@ -101,16 +101,16 @@ %s підписується на вас Тут нічого немає. Потягніть вниз, щоб оновити! Тут нічого немає. - Згорнути - Розгорнути - Натисніть для перегляду - Медіа приховано + Згорнути + Розгорнути + Натисніть для перегляду + Медіа приховано Попередження про вміст Змінити Написати Скасувати приглушення розмови Заглушити розмову - Заплановані дмухи + Заплановані дмухи Підписники Написати Медіа @@ -130,16 +130,16 @@ Розгорнути Прибрати просування Просунути - %s вподобує ваш дмух - %s просуває ваш дмух - Згорнути - Розгорнути - Делікатний вміст - %s просуває + %s вподобує ваш допис + %s просуває ваш допис + Згорнути + Розгорнути + Делікатний вміст + %s просуває Приховані домени Заглушені користувачі - Дописи - Дмухнути + Дописи + Тред Вкладки Не вдалося відвантажити. Не вдалося отримати токен входу. @@ -149,17 +149,17 @@ Введено недійсний домен Показати просування Показати просування - Вкладки + Вкладки Не глушити %s - Видимість дмухів + Видимість дописів Деякі відомості, які можуть вплинути на ваше психічний стан, буде приховано. Це включає: \n \n - Вподобання/Просування/Сповіщення про підписки -\n - Вподобання/Кількість просувань дмухів +\n - Вподобання/Кількість просувань дописів \n - Статистика підписників/Публікацій у профілях \n \n На push-сповіщення це не вплине, але ви можете переглянути налаштування сповіщень вручну. - Вподобано + Вподобано Вподобали %1$s вподобання @@ -167,7 +167,7 @@ %1$s вподобань %1$s вподобань - Сповіщати про вподобання кимось дмухів + Сповіщати про вподобання кимось дописів мої дописи вподобано Сховати медіа Заглушити сповіщення від %s @@ -256,9 +256,9 @@ %s голосів %1$s • %2$s - Без опису - Попередження про вміст: %s - Медіа: %s + Без опису + Попередження про вміст: %s + Медіа: %s Просунули Вміст CC-BY-SA 4.0 @@ -268,13 +268,13 @@ Пізніше Вам потрібно буде перезапустити Tusky, щоб застосувати ці зміни Необхідно перезапустити застосунок - Відкрити дмух - Розгорнути/згорнути всі статуси - Копію дмуху збережено до ваших чернеток - Надсилання скасовано - Надсилання дмухів - Помилка надсилання дмуху - Надсилання дмуху… + Відкрити дмух + Розгорнути/згорнути всі статуси + Копію дмуху збережено до ваших чернеток + Надсилання скасовано + Надсилання дмухів + Помилка надсилання дмуху + Надсилання дмуху… Оприлюднення з облікового запису %1$s Вилучити обліковий запис зі списку Додати обліковий запис до списку @@ -302,7 +302,7 @@ Загальнодоступні стрічки завантажити ще Відповідь для @%s - Завжди розгортати дмухи, з попередженнями про вміст + Завжди розгортати допис, з попередженнями про вміст Підписники Завжди показувати делікатний вміст %dс @@ -316,12 +316,12 @@ за %dдн за %dр. Запит на підписку надіслано - Вкладення - Звуки - Відео - Зображення - Поділитися посиланням на дмух - Поділитися вмістом дмуху + Вкладення + Звуки + Відео + Зображення + Поділитися посиланням на допис + Поділитися вмістом допису Профіль Tusky Звіти про вади та запити функцій: \n https://github.com/tuskyapp/Tusky/issues @@ -344,23 +344,23 @@ %1$s, %2$s, та %3$s %1$s, %2$s, %3$s та %4$d інших %s згадує вас - Сповіщати про нові дмухи осіб, на яких ви підписалися - Нові дмухи + Сповіщати про нові дописи осіб, на яких ви підписалися + Нові дописи Сповіщати про завершення опитувань Опитування - Сповіщати про просування кимось + Сповіщати про просування кимось допису Просування Сповіщати про нові запити на підписки Сповіщати про нових підписників Нові підписники Сповіщати про нові згадки Нові згадки - Найбільший - Великий - Середній - Маленький - Найменший - Розмір шрифту статусу + Найбільший + Великий + Середній + Маленький + Найменший + Розмір шрифту статусу Лише для підписників Приховано Приховано @@ -380,7 +380,7 @@ Проксі Завантаження попереднього перегляду медіа Показати відповіді - Фільтрування стрічки + Фільтрування стрічки Анімувати власні емодзі Показувати барвисті градієнти замість прихованих медіа Анімовані GIF-аватарки @@ -398,7 +398,7 @@ Стрічки Тема застосунку Вигляд - хтось, на кого мене підписано, публікує новий дмух + хтось, на кого мене підписано, публікує новий допис опитування завершено мої дописи просунуто отримано запит на підписку @@ -418,8 +418,8 @@ Заблокувати @%s\? Сховати весь домен Ви впевнені, що хочете заблокувати все з %s\? Ви не побачите вміст із цього домену в жодних загальнодоступних стрічках або у своїх сповіщеннях. Ваших підписників з цього домену буде видалено. - Видалити й переписати цей дмух\? - Видалити цей дмух\? + Видалити й переписати цей дмух\? + Видалити цей дмух\? Не стежити за цим обліковим записом\? Відкликати запит на підписку\? Завантаження @@ -438,13 +438,13 @@ Аватар Відповісти… Показуване ім\'я - Відповідь успішно надіслано. + Відповідь успішно надіслано. %s показано Глушіння користувача прибрано Користувача розблоковано Поділитися медіа з… - Поділитися дмухом з… - Поділитися URL-адресою дмуха з… + Поділитися дописом з… + Поділитися URL-адресою допису з… Завантаження медіа Завантажити медіа Відкрити медіа #%d @@ -453,12 +453,12 @@ Хештеги Відкрити автора просування Додати вкладку - Запланувати дмух + Запланувати дмух Клавіотура емодзі - Дмух, для якого ви створили чернетку відповіді, вилучено + Дмух, для якого ви створили чернетку відповіді, вилучено Чернетку видалено Не вдалося завантажити дані відповіді - Не вдалося надіслати цей дмух! + Не вдалося надіслати цей дмух! Ви дійсно хочете видалити список %s\? Ви не можете завантажити більше ніж %1$d медіавкладення. @@ -477,7 +477,7 @@ Показувати попередній перегляд посилань у стрічках Найкоротший час планування Mastodon становить 5 хвилин. Оголошень немає. - Черга статусів порожня. + Черга статусів порожня. У вас немає чернеток. Помилка пошуку допису %s Увімкнути перемикання між вкладками жестом проведення пальцем @@ -485,11 +485,11 @@ Не вдалося здійснити пошук Обліковий запис з іншого сервера. Надіслати анонімізовану копію звіту й туди\? Скаргу буде надіслано вашому модератору сервера. Ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис знизу: - Не вдалося отримати статуси + Не вдалося отримати статуси Переслати до %s Дії для зображення %s Ви впевнені, що хочете остаточно очистити всі сповіщення\? - Створити дмух + Створити допис Застосувати Фільтр Очистити @@ -500,8 +500,8 @@ Назва списку Опитування з варіантами: %1$s, %2$s, %3$s, %4$s; %5$s Безпосередньо - Додано до закладок - Просунуто + Додано до закладок + Просунуто досягнено обмеження %1$d вкладка досягнено обмеження %1$d вкладки @@ -534,4 +534,11 @@ Видалити бесіду Вилучити закладку Запитувати підтвердження перед додаванням до вподобаних + 14 днів + 30 днів + 60 днів + 90 днів + 180 днів + 365 днів + Створити допис \ No newline at end of file diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml index 78d32747..586c7f34 100644 --- a/app/src/main/res/values-v27/styles.xml +++ b/app/src/main/res/values-v27/styles.xml @@ -1,15 +1,5 @@ - - -