diff --git a/app/build.gradle b/app/build.gradle index 4766dc8ec..20d306505 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,6 +74,9 @@ android { androidExtensions { experimental = true } + buildFeatures { + viewBinding true + } testOptions { unitTests { returnDefaultValues = true @@ -144,7 +147,7 @@ dependencies { implementation "androidx.room:room-rxjava2:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" - implementation "com.google.android.material:material:1.2.1" + implementation "com.google.android.material:material:1.3.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 78b4be883..a05994a10 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -43,6 +43,10 @@ public *; } +-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type { + public *; +} + # preserve line numbers for crash reporting -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json new file mode 100644 index 000000000..01a491b4a --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json @@ -0,0 +1,821 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "e2cb844862443c2c5cc884c11f120d43", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "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, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, 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": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "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 + } + ], + "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_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` 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.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.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, 'e2cb844862443c2c5cc884c11f120d43')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eeb7c12af..723021ee5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,6 +146,7 @@ + { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - } private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 949557cc3..4bc79ce94 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -78,7 +78,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private val viewModel: AccountViewModel by viewModels { viewModelFactory } - private val accountFieldAdapter = AccountFieldAdapter(this) + private lateinit var accountFieldAdapter : AccountFieldAdapter private var followState: FollowState = FollowState.NOT_FOLLOWING private var blocking: Boolean = false @@ -89,6 +89,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private var loadedAccount: Account? = null private var animateAvatar: Boolean = false + private var animateEmojis: Boolean = false // fields for scroll animation private var hideFab: Boolean = false @@ -124,6 +125,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) + animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) hideFab = sharedPrefs.getBoolean("fabHide", false) setupToolbar() @@ -162,6 +164,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFollowsYouTextView.hide() // setup the RecyclerView for the account fields + accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) accountFieldList.isNestedScrollingEnabled = false accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.adapter = accountFieldAdapter @@ -375,9 +378,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val usernameFormatted = getString(R.string.status_username_format, account.username) accountUsernameTextView.text = usernameFormatted - accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView) + accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView) + val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis) LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() @@ -437,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateToolbar() { loadedAccount?.let { account -> - val emojifiedName = account.name.emojify(account.emojis, accountToolbar) + val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis) try { supportActionBar?.title = EmojiCompat.get().process(emojifiedName) @@ -565,11 +568,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI subscribing = relation.subscribing } + // remove the listener so it doesn't fire on non-user changes + accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + accountNoteTextInputLayout.visible(relation.note != null) accountNoteTextInputLayout.editText?.setText(relation.note) - // add the listener late to avoid it firing on the first change - accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) updateButtons() @@ -619,8 +623,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if(subscribing) { accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) } else { accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) } } @@ -650,14 +656,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI menuInflater.inflate(R.menu.account_toolbar, menu) if (!viewModel.isSelf) { - val follow = menu.findItem(R.id.action_follow) - follow.title = if (followState == FollowState.NOT_FOLLOWING) { - getString(R.string.action_follow) - } else { - getString(R.string.action_unfollow) - } - - follow.isVisible = followState != FollowState.REQUESTED val block = menu.findItem(R.id.action_block) block.title = if (blocking) { @@ -701,8 +699,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } else { - // It shouldn't be possible to block, follow, mute or report yourself. - menu.removeItem(R.id.action_follow) + // It shouldn't be possible to block, mute or report yourself. menu.removeItem(R.id.action_block) menu.removeItem(R.id.action_mute) menu.removeItem(R.id.action_mute_domain) @@ -759,8 +756,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI showMuteAccountDialog( this, it.username - ) { notifications -> - viewModel.muteAccount(notifications) + ) { notifications, duration -> + viewModel.muteAccount(notifications, duration) } } } else { @@ -794,14 +791,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - R.id.action_mention -> { - mention() - return true - } R.id.action_open_in_web -> { // If the account isn't loaded yet, eat the input. if (loadedAccount != null) { @@ -809,10 +798,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } return true } - R.id.action_follow -> { - viewModel.changeFollowState() - return true - } R.id.action_block -> { toggleBlock() return true diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index 6cf3367eb..d592f0531 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import com.keylesspalace.tusky.fragment.AccountListFragment import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector @@ -68,16 +67,6 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { .commit() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = dispatchingAndroidInjector companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 2933d6893..f1c3d54dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.State @@ -71,7 +72,9 @@ class AccountsInListFragment : DialogFragment(), Injectable { private val searchAdapter = SearchAdapter() private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } - private val animateAvatar by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("animateGifAvatars", false) } + private val pm by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) } + private val animateAvatar by lazy { pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) } + private val animateEmojis by lazy { pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -209,7 +212,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { } fun bind(account: Account) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) + displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) usernameTextView.text = account.username loadAvatar(account.avatar, avatar, radius, animateAvatar) } @@ -252,7 +255,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { override val containerView = itemView fun bind(account: Account, inAList: Boolean) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) + displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) usernameTextView.text = account.username loadAvatar(account.avatar, avatar, radius, animateAvatar) diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 363872684..92994f165 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.util.Log; +import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; @@ -127,6 +128,15 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + @Override public void finish() { super.finish(); diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 3d86b6e12..64d952b99 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -296,10 +296,6 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } R.id.action_save -> { save() return true diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 5adce8edb..0726b26e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -1,7 +1,6 @@ package com.keylesspalace.tusky import android.os.Bundle -import android.view.MenuItem import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Toast @@ -205,14 +204,4 @@ class FiltersActivity: BaseActivity() { } } - // Activate back arrow in toolbar - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt index 19f66e85b..b61ae65e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.os.Bundle import androidx.annotation.RawRes import android.util.Log -import android.view.MenuItem import android.widget.TextView import com.keylesspalace.tusky.util.IOUtils import kotlinx.android.extensions.CacheImplementation @@ -49,16 +48,6 @@ class LicenseActivity : BaseActivity() { } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { val sb = StringBuilder() diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 3894f652d..fa3c92c3d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* @@ -130,19 +129,27 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { else R.string.action_rename_list) { _, _ -> onPickedDialogName(editText.text, list?.id) } - .setNegativeButton(android.R.string.cancel) { d, _ -> - d.dismiss() - } + .setNegativeButton(android.R.string.cancel, null) .show() val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) editText.onTextChanged { s, _, _, _ -> - positiveButton.isEnabled = !s.isBlank() + positiveButton.isEnabled = s.isNotBlank() } editText.setText(list?.title) editText.text?.let { editText.setSelection(it.length) } } + private fun showListDeleteDialog(list: MastoList) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) + .setPositiveButton(R.string.action_delete){ _, _ -> + viewModel.deleteList(list.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + private fun update(state: ListsViewModel.State) { adapter.submitList(state.lists) @@ -199,7 +206,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { when (item.itemId) { R.id.list_edit -> openListSettings(list) R.id.list_rename -> renameListDialog(list) - R.id.list_delete -> viewModel.deleteList(list.id) + R.id.list_delete -> showListDeleteDialog(list) else -> return@setOnMenuItemClickListener false } true @@ -210,14 +217,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { override fun androidInjector() = dispatchingAndroidInjector - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - private object ListsDiffer : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { return oldItem.id == newItem.id diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index bd216776c..e6070149c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -20,14 +20,13 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.net.Uri -import android.os.Build import android.os.Bundle import android.text.method.LinkMovementMethod import android.util.Log -import android.view.MenuItem 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.di.Injectable @@ -109,14 +108,6 @@ class LoginActivity : BaseActivity(), Injectable { } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - /** * 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 @@ -339,16 +330,19 @@ class LoginActivity : BaseActivity(), Injectable { private fun openInCustomTab(uri: Uri, context: Context): Boolean { val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) - val customTabsIntentBuilder = CustomTabsIntent.Builder() + 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() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - customTabsIntentBuilder.setNavigationBarColor( - ThemeUtils.getColor(context, android.R.attr.navigationBarColor) - ) - } + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .build() - val customTabsIntent = customTabsIntentBuilder.build() try { customTabsIntent.launchUrl(context, uri) } catch (e: ActivityNotFoundException) { diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index d1f99c56c..577126a1e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.core.content.pm.ShortcutManagerCompat import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat.InitCallback @@ -61,11 +62,14 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.conversation.ConversationsRepository +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.fragment.NotificationsFragment @@ -115,6 +119,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje @Inject lateinit var conversationRepository: ConversationsRepository + @Inject + lateinit var appDb: AppDatabase + + @Inject + lateinit var draftHelper: DraftHelper + @Inject lateinit var viewModelFactory: ViewModelFactory @@ -262,6 +272,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje // Flush old media that was cached for sharing deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) } + draftWarning() } override fun onResume() { @@ -448,7 +459,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje nameRes = R.string.action_access_saved_toot iconRes = R.drawable.ic_notebook onClick = { - val intent = Intent(context, SavedTootActivity::class.java) + val intent = DraftsActivity.newIntent(context) startActivityWithSlideInAnimation(intent) } }, @@ -733,6 +744,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) + mainToolbar.setOnClickListener { + (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() + } keepScreenOn() return popups @@ -783,6 +797,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) cacheUpdater.clearForUser(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id) + draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) removeShortcut(this, activeAccount) val newAccount = accountManager.logActiveAccountOut() if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) { @@ -861,16 +876,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje .into(object : CustomTarget(navIconSize, navIconSize) { override fun onLoadStarted(placeholder: Drawable?) { - if(placeholder != null) { + if (placeholder != null) { mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } override fun onResourceReady(resource: Drawable, transition: Transition?) { - mainToolbar.navigationIcon = resource + mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) } override fun onLoadCleared(placeholder: Drawable?) { - mainToolbar.navigationIcon = placeholder + if (placeholder != null) { + mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } } }) } @@ -895,8 +912,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun updateProfiles() { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header)) + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) ProfileDrawerItem().apply { isSelected = acc.isActive @@ -920,6 +938,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje header.setActiveProfile(accountManager.activeAccount!!.id) } + private fun draftWarning() { + val sharedPrefsKey = "show_draft_warning" + appDb.tootDao().savedTootCount() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { draftCount -> + val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true) + if (draftCount > 0 && showDraftWarning) { + AlertDialog.Builder(this) + .setMessage(R.string.new_drafts_warning) + .setNegativeButton("Don't show again") { _, _ -> + preferences.edit(commit = true) { + putBoolean(sharedPrefsKey, false) + } + } + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + + } + override fun getActionButton(): FloatingActionButton? = composeButton override fun androidInjector() = androidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index a43e2d080..de397fce2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -3,7 +3,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -80,14 +79,6 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn override fun getActionButton(): FloatingActionButton? = null - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - override fun androidInjector() = dispatchingAndroidInjector } diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 9901cf85a..4ccd330a4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -18,7 +18,6 @@ package com.keylesspalace.tusky; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; -import android.view.MenuItem; import android.view.View; import androidx.annotation.Nullable; @@ -89,7 +88,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); if (bar != null) { - bar.setTitle(getString(R.string.title_saved_toot)); + bar.setTitle(getString(R.string.title_drafts)); bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayShowHomeEnabled(true); } @@ -118,17 +117,6 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd if (asyncTask != null) asyncTask.cancel(true); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - private void fetchToots() { asyncTask = new FetchPojosTask(this, database.tootDao()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -166,6 +154,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd ComposeOptions composeOptions = new ComposeOptions( /*scheduledTootUid*/null, item.getUid(), + /*drafId*/null, item.getText(), jsonUrls, descriptions, @@ -180,6 +169,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd item.getInReplyToUsername(), item.getInReplyToText(), /*mediaAttachments*/null, + /*draftAttachments*/null, /*scheduledAt*/null, /*sensitive*/null, /*poll*/null, diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index c092a014a..7a5a49fc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.activity.viewModels import androidx.fragment.app.commit import androidx.lifecycle.Lifecycle @@ -85,14 +84,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { floating_btn.setOnClickListener(viewQuickToot::onFABClicked) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home){ - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = dispatchingAndroidInjector companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 9fcacec38..aad5b5459 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.MenuItem import android.view.View import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog @@ -345,14 +344,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - override fun onPause() { super.onPause() if (tabsChanged) { diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index b66908b7c..6f0e8ee17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -18,7 +18,6 @@ package com.keylesspalace.tusky; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.view.MenuItem; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -95,17 +94,6 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn .subscribe(quickTootView::handleEvent); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - @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 e2ae63d17..88fb88cc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -110,10 +110,6 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } case R.id.action_open_in_web: { LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this); return true; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 5c52e39ee..24430dcec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -33,10 +33,14 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { List accountList; AccountActionListener accountActionListener; private boolean bottomLoading; + protected final boolean animateEmojis; + protected final boolean animateAvatar; - AccountAdapter(AccountActionListener accountActionListener) { + AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { this.accountList = new ArrayList<>(); this.accountActionListener = accountActionListener; + this.animateAvatar = animateAvatar; + this.animateEmojis = animateEmojis; bottomLoading = false; } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt index 5e8f25ce7..551bd6d67 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -31,7 +31,7 @@ import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify import kotlinx.android.synthetic.main.item_account_field.view.* -class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter() { +class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter() { var emojis: List = emptyList() var fields: List> = emptyList() @@ -57,10 +57,10 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { val field = proofOrField.asRight() - val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView) + val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis) viewHolder.nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView) + val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis) LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) if(field.verifiedAt != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt index dae0db4b6..c8df79f9f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -23,6 +23,7 @@ import android.widget.ArrayAdapter import androidx.preference.PreferenceManager import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.item_autocomplete_account.view.* @@ -41,12 +42,14 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter(co val username = view.username val displayName = view.display_name val avatar = view.avatar + val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context) + val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + username.text = account.fullName - displayName.text = account.displayName.emojify(account.emojis, displayName) + displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis) val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) - val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context) - .getBoolean("animateGifAvatars", false) + val animateAvatar = pm.getBoolean("animateGifAvatars", false) loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) 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 7b07d5bd9..559426e38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -22,7 +22,6 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { private ImageView avatarInset; private String accountId; private boolean showBotOverlay; - private boolean animateAvatar; public AccountViewHolder(View itemView) { super(itemView); @@ -32,15 +31,14 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { avatarInset = itemView.findViewById(R.id.account_avatar_inset); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); - animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false); } - public void setupWithAccount(Account account) { + public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { accountId = account.getId(); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); int avatarRadius = avatar.getContext().getResources() .getDimensionPixelSize(R.dimen.avatar_radius_48dp); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 073d76dab..13144cb87 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -34,8 +34,8 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper; public class BlocksAdapter extends AccountAdapter { - public BlocksAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -60,7 +60,7 @@ public class BlocksAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } @@ -71,7 +71,6 @@ public class BlocksAdapter extends AccountAdapter { private TextView displayName; private ImageButton unblock; private String id; - private boolean animateAvatar; BlockedUserViewHolder(View itemView) { super(itemView); @@ -79,14 +78,12 @@ public class BlocksAdapter extends AccountAdapter { username = itemView.findViewById(R.id.blocked_user_username); displayName = itemView.findViewById(R.id.blocked_user_display_name); unblock = itemView.findViewById(R.id.blocked_user_unblock); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); } - void setupWithAccount(Account account) { + void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 821587463..98cb9e4df 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener; /** Both for follows and following lists. */ public class FollowAdapter extends AccountAdapter { - public FollowAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -53,7 +53,7 @@ public class FollowAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { AccountViewHolder holder = (AccountViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } 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 dec4586ba..8fa14731c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -10,27 +10,24 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener -import com.keylesspalace.tusky.util.emojify -import com.keylesspalace.tusky.util.loadAvatar -import com.keylesspalace.tusky.util.unicodeWrap -import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.item_follow_request_notification.view.* -internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { +internal class FollowRequestViewHolder( + itemView: View, + private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { private var id: String? = null - private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context) - .getBoolean("animateGifAvatars", false) - fun setupWithAccount(account: Account) { + fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { id = account.id val wrappedName = account.name.unicodeWrap() - val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView) + val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) itemView.displayNameTextView.text = emojifiedName if (showHeader) { val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - }.emojify(account.emojis, itemView) + }.emojify(account.emojis, itemView, animateEmojis) } itemView.notificationTextView?.visible(showHeader) val format = itemView.context.getString(R.string.status_username_format) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index dab3d4fe3..9ba598842 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener; public class FollowRequestsAdapter extends AccountAdapter { - public FollowRequestsAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index c4224c9c9..e1a30759a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -23,8 +23,8 @@ import java.util.HashMap; public class MutesAdapter extends AccountAdapter { private HashMap mutingNotificationsMap; - public MutesAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); mutingNotificationsMap = new HashMap(); } @@ -51,7 +51,7 @@ public class MutesAdapter extends AccountAdapter { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; Account account = accountList.get(position); - holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId())); + holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } @@ -73,7 +73,6 @@ public class MutesAdapter extends AccountAdapter { private ImageButton unmute; private ImageButton muteNotifications; private String id; - private boolean animateAvatar; private boolean notifications; MutedUserViewHolder(View itemView) { @@ -83,13 +82,11 @@ public class MutesAdapter extends AccountAdapter { displayName = itemView.findViewById(R.id.muted_user_display_name); unmute = itemView.findViewById(R.id.muted_user_unmute); muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); } - void setupWithAccount(Account account, Boolean mutingNotifications) { + void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); 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 9a6873f78..002c7d09d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -236,7 +236,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW_REQUEST: { if (payloadForHolder == null) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(concreteNotificaton.getAccount()); + holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); holder.setupActionListener(accountActionListener); } } @@ -260,6 +260,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { CardViewMode.NONE, statusDisplayOptions.confirmReblogs(), statusDisplayOptions.hideStats(), + statusDisplayOptions.animateEmojis(), statusDisplayOptions.quoteEnabled() ); } @@ -341,13 +342,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { String format = context.getString(R.string.notification_follow_format); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wholeMessage = String.format(format, wrappedDisplayName); - CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message); + CharSequence emojifiedMessage = CustomEmojiHelper.emojify( + wholeMessage, account.getEmojis(), message, statusDisplayOptions.animateEmojis() + ); message.setText(emojifiedMessage); String username = context.getString(R.string.status_username_format, account.getUsername()); usernameView.setText(username); - CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView); + CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify( + wrappedDisplayName, account.getEmojis(), usernameView, statusDisplayOptions.animateEmojis() + ); displayNameView.setText(emojifiedDisplayName); @@ -433,7 +438,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } private void setDisplayName(String name, List emojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, statusDisplayOptions.animateEmojis()); displayName.setText(emojifiedName); } @@ -527,7 +532,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - CharSequence emojifiedText = CustomEmojiHelper.emojify(str, notificationViewData.getAccount().getEmojis(), message); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis() + ); message.setText(emojifiedText); if (statusViewData != null) { @@ -650,11 +657,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusContent.setFilters(NO_INPUT_FILTER); } - CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + content, emojis, statusContent, statusDisplayOptions.animateEmojis() + ); LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); - CharSequence emojifiedContentWarning = - CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); + CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify( + statusViewData.getSpoilerText(), + statusViewData.getStatusEmojis(), + contentWarningDescriptionTextView, + statusDisplayOptions.animateEmojis() + ); contentWarningDescriptionTextView.setText(emojifiedContentWarning); setQuoteContainer(statusViewData.getQuote(), listener, statusDisplayOptions); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index a990d326a..0208b9530 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -38,6 +38,7 @@ class PollAdapter: RecyclerView.Adapter() { private var mode = RESULT private var emojis: List = emptyList() private var resultClickListener: View.OnClickListener? = null + private var animateEmojis = false fun setup( options: List, @@ -45,13 +46,15 @@ class PollAdapter: RecyclerView.Adapter() { votersCount: Int?, emojis: List, mode: Int, - resultClickListener: View.OnClickListener?) { + resultClickListener: View.OnClickListener?, + animateEmojis: Boolean) { this.pollOptions = options this.voteCount = voteCount this.votersCount = votersCount this.emojis = emojis this.mode = mode this.resultClickListener = resultClickListener + this.animateEmojis = animateEmojis notifyDataSetChanged() } @@ -81,7 +84,7 @@ class PollAdapter: RecyclerView.Adapter() { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) - .emojify(emojis, holder.resultTextView) + .emojify(emojis, holder.resultTextView, animateEmojis) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 @@ -90,7 +93,7 @@ class PollAdapter: RecyclerView.Adapter() { holder.resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) + val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis) holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.radioButton.isChecked = option.selected holder.radioButton.setOnClickListener { @@ -101,7 +104,7 @@ class PollAdapter: RecyclerView.Adapter() { } } MULTIPLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox) + val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis) holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.checkBox.isChecked = option.selected holder.checkBox.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index de93c3a47..2cb2ee9ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -190,8 +190,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected abstract int getMediaPreviewHeight(Context context); - protected void setDisplayName(String name, List customEmojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName); + protected void setDisplayName(String name, List customEmojis, StatusDisplayOptions statusDisplayOptions) { + CharSequence emojifiedName = CustomEmojiHelper.emojify( + name, customEmojis, displayName, statusDisplayOptions.animateEmojis() + ); displayName.setText(emojifiedName); } @@ -215,7 +217,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { final StatusActionListener listener) { boolean sensitive = !TextUtils.isEmpty(spoilerText); if (sensitive) { - CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); + CharSequence emojiSpoiler = CustomEmojiHelper.emojify( + spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis() + ); contentWarningDescription.setText(emojiSpoiler); contentWarningDescription.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE); @@ -254,7 +258,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, final StatusActionListener listener) { if (expanded) { - CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); + CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis()); LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); for (int i = 0; i < mediaLabels.length; ++i) { updateMediaLabel(i, sensitive, expanded); @@ -613,7 +617,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { @DrawableRes private static int getLabelIcon(Attachment.Type type) { switch (type) { - default: case IMAGE: return R.drawable.ic_photo_24dp; case GIFV: @@ -621,6 +624,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return R.drawable.ic_videocam_24dp; case AUDIO: return R.drawable.ic_music_box_24dp; + default: + return R.drawable.ic_attach_file_24dp; } } @@ -814,7 +819,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { if (payloads == null) { - setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions); setUsername(status.getNickname()); setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setStatusVisibility(status.getVisibility()); @@ -826,7 +831,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setBookmarked(status.isBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.isSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { @@ -876,13 +881,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - protected static boolean hasAudioAttachment(List attachments) { + protected static boolean hasPreviewableAttachment(List attachments) { for (Attachment attachment : attachments) { - if (attachment.getType() == Attachment.Type.AUDIO) { - return true; + if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { + return false; } } - return false; + return true; } private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, @@ -1035,12 +1040,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { listener.onViewThread(position); } }; - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + PollAdapter.RESULT, + viewThreadListener, + statusDisplayOptions.animateEmojis() + ); pollButton.setVisibility(View.GONE); } else { // voting possible - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, + null, + statusDisplayOptions.animateEmojis() + ); pollButton.setVisibility(View.VISIBLE); 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 56cef6ee1..043b7b35e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -27,8 +27,10 @@ import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; import at.connyduck.sparkbutton.helpers.Utils; @@ -64,7 +66,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { if (rebloggedByDisplayName == null) { hideStatusInfo(); } else { - setRebloggedByDisplayName(rebloggedByDisplayName); + setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); } @@ -73,10 +75,16 @@ public class StatusViewHolder extends StatusBaseViewHolder { } - private void setRebloggedByDisplayName(final String name) { + private void setRebloggedByDisplayName(final CharSequence name, + final StatusViewData.Concrete status, + final StatusDisplayOptions statusDisplayOptions) { Context context = statusInfo.getContext(); - String boostedText = context.getString(R.string.status_boosted_format, name); - statusInfo.setText(boostedText); + CharSequence wrappedName = StringUtils.unicodeWrap(name); + CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis() + ); + statusInfo.setText(emojifiedText); statusInfo.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 34d3687fb..3a8b95d97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -67,6 +67,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter { statusDisplayOptions.cardViewMode(), statusDisplayOptions.confirmReblogs(), statusDisplayOptions.hideStats(), + statusDisplayOptions.animateEmojis(), statusDisplayOptions.quoteEnabled() ); } 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 c0e6bdd8e..b54b15554 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 @@ -42,7 +42,8 @@ interface AnnouncementActionListener: LinkListener { class AnnouncementAdapter( private var items: List = emptyList(), private val listener: AnnouncementActionListener, - private val wellbeingEnabled: Boolean = false + private val wellbeingEnabled: Boolean = false, + private val animateEmojis: Boolean = false ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { @@ -99,7 +100,8 @@ class AnnouncementAdapter( reaction.staticUrl ?: "", null )), - this + this, + animateEmojis ) isChecked = reaction.me 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 1e20cb033..bc0c60462 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 @@ -19,7 +19,6 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Bundle -import android.view.MenuItem import android.view.View import android.widget.PopupWindow import androidx.activity.viewModels @@ -83,8 +82,9 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled) + adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) announcementsList.adapter = adapter @@ -123,16 +123,6 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, progressBar.show() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun refreshAnnouncements() { viewModel.load() swipeRefreshLayout.isRefreshing = true 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 1ebaa15c6..b696adbc3 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 @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.compose import android.Manifest import android.app.Activity import android.app.ProgressDialog -import android.app.TimePickerDialog import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -32,7 +31,6 @@ import android.os.Build import android.os.Bundle import android.os.Parcelable import android.provider.MediaStore -import android.text.TextUtils import android.util.Log import android.view.KeyEvent import android.view.MenuItem @@ -60,7 +58,6 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.appstore.EventHub @@ -68,13 +65,16 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener +import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -86,7 +86,6 @@ import java.io.File import java.io.IOException import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList import kotlin.math.max import kotlin.math.min @@ -96,7 +95,7 @@ class ComposeActivity : BaseActivity(), OnEmojiSelectedListener, Injectable, InputConnectionCompat.OnCommitContentListener, - TimePickerDialog.OnTimeSetListener { + ComposeScheduleView.OnTimeSetListener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -111,10 +110,10 @@ class ComposeActivity : BaseActivity(), // this only exists when a status is trying to be sent, but uploads are still occurring private var finishingUploadDialog: ProgressDialog? = null private var photoUploadUri: Uri? = null + @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT - private var composeOptions: ComposeOptions? = null private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private val maxUploadMediaNumber = 4 @@ -155,28 +154,28 @@ class ComposeActivity : BaseActivity(), /* If the composer is started up as a reply to another post, override the "starting" state * based on what the intent from the reply request passes. */ - if (intent != null) { - this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) - viewModel.setup(composeOptions) - setupReplyViews(composeOptions?.replyingStatusAuthor) - setupQuoteView(composeOptions?.quoteStatusAuthor) - val tootText = composeOptions?.tootText - if (!tootText.isNullOrEmpty()) { - composeEditField.setText(tootText) - } + + val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) + + viewModel.setup(composeOptions) + setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) + setupQuoteView(composeOptions?.quoteStatusAuthor, composeOptions?.quoteStatusContent) + val tootText = composeOptions?.tootText + if (!tootText.isNullOrEmpty()) { + composeEditField.setText(tootText) } - if (loadInstanceData(preferences)) { + if (loadInstanceData(preferences, composeOptions?.tootRightNow == true)) { viewModel.loadInstanceDataFromNetwork() } else { viewModel.loadInstanceDataFromCache() } - if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) { + if (!composeOptions?.scheduledAt.isNullOrEmpty()) { composeScheduleView.setDateTime(composeOptions?.scheduledAt) } - setupComposeField(viewModel.startingText) + setupComposeField(preferences, viewModel.startingText) setupDefaultTagViews(preferences) setupContentWarningField(composeOptions?.contentWarning) setupPollView() @@ -188,8 +187,8 @@ class ComposeActivity : BaseActivity(), } } - private fun loadInstanceData(preferences: SharedPreferences): Boolean { - if (composeOptions?.tootRightNow == true) { + private fun loadInstanceData(preferences: SharedPreferences, tootRightNow: Boolean): Boolean { + if (tootRightNow) { return false // from Quick Toot } if (!preferences.getBoolean("limitedBandwidthActive", false)) { @@ -214,38 +213,24 @@ class ComposeActivity : BaseActivity(), return false } - private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) { - if (intent != null && savedInstanceState == null) { + private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) { + if (savedInstanceState == null) { /* Get incoming images being sent through a share action from another app. Only do this * when savedInstanceState is null, otherwise both the images from the intent and the * instance state will be re-queued. */ - val type = intent.type - if (type != null) { + intent.type?.also { type -> if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { - val uriList = ArrayList() - if (intent.action != null) { - when (intent.action) { - Intent.ACTION_SEND -> { - val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) - if (uri != null) { - uriList.add(uri) - } - } - Intent.ACTION_SEND_MULTIPLE -> { - val list = intent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM) - if (list != null) { - for (uri in list) { - if (uri != null) { - uriList.add(uri) - } - } - } + when (intent.action) { + Intent.ACTION_SEND -> { + intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> + pickMedia(uri) + } + } + Intent.ACTION_SEND_MULTIPLE -> { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.forEach { uri -> + pickMedia(uri) } } - } - for (uri in uriList) { - pickMedia(uri) } } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { @@ -263,13 +248,16 @@ class ComposeActivity : BaseActivity(), val left = min(start, end) val right = max(start, end) composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + // move edittext cursor to first when shareBody parsed + composeEditField.text.insert(0, "\n") + composeEditField.setSelection(0) } } } } } - private fun setupReplyViews(replyingStatusAuthor: String?) { + private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { if (replyingStatusAuthor != null) { composeReplyView.show() composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) @@ -293,10 +281,10 @@ class ComposeActivity : BaseActivity(), } } } - composeOptions?.replyingStatusContent?.let { composeReplyContentView.text = it } + replyingStatusContent?.let { composeReplyContentView.text = it } } - private fun setupQuoteView(quoteStatusAuthor: String?) { + private fun setupQuoteView(quoteStatusAuthor: String?, quoteStatusContent: String?) { if (quoteStatusAuthor != null) { composeQuoteView.show() composeQuoteView.text = getString(R.string.quote_to, quoteStatusAuthor) @@ -320,7 +308,7 @@ class ComposeActivity : BaseActivity(), } } } - composeOptions?.quoteStatusContent?.let { composeQuoteContentView.text = it } + quoteStatusContent?.let { composeQuoteContentView.text = it } } private fun setupContentWarningField(startingContentWarning: String?) { @@ -330,13 +318,18 @@ class ComposeActivity : BaseActivity(), composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } } - private fun setupComposeField(startingText: String?) { + private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { composeEditField.setOnCommitContentListener(this) composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } composeEditField.setAdapter( - ComposeAutoCompleteAdapter(this)) + ComposeAutoCompleteAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + ) composeEditField.setTokenizer(ComposeTokenizer()) composeEditField.setText(startingText) @@ -446,6 +439,7 @@ class ComposeActivity : BaseActivity(), composeHideMediaButton.setOnClickListener { toggleHideMedia() } composeScheduleButton.setOnClickListener { onScheduleClick() } composeScheduleView.setResetOnClickListener { resetSchedule() } + composeScheduleView.setListener(this) atButton.setOnClickListener { atButtonClicked() } hashButton.setOnClickListener { hashButtonClicked() } @@ -743,7 +737,6 @@ class ComposeActivity : BaseActivity(), } } - private fun removePoll() { viewModel.poll.value = null pollPreview.hide() @@ -934,22 +927,22 @@ class ComposeActivity : BaseActivity(), override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { - if(intent.data != null){ + if (intent.data != null) { // Single media, upload it and done. pickMedia(intent.data!!) - }else if(intent.clipData != null){ + } else if (intent.clipData != null) { val clipData = intent.clipData!! val count = clipData.itemCount - if(mediaCount + count > maxUploadMediaNumber){ + if (mediaCount + count > maxUploadMediaNumber) { // check if exist media + upcoming media > 4, then prob error message. Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() - }else{ + } else { // if not grater then 4, upload all multiple media. for (i in 0 until count) { - val imageUri = clipData.getItemAt(i).getUri() - pickMedia(imageUri) - } + val imageUri = clipData.getItemAt(i).getUri() + pickMedia(imageUri) } + } } } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { pickMedia(photoUploadUri!!) @@ -1099,9 +1092,8 @@ class ComposeActivity : BaseActivity(), } } - override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { - composeScheduleView.onTimeSet(hourOfDay, minute) - viewModel.updateScheduledAt(composeScheduleView.time) + override fun onTimeSet(time: String) { + viewModel.updateScheduledAt(time) if (verifyScheduledTime()) { scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { @@ -1117,8 +1109,9 @@ class ComposeActivity : BaseActivity(), @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin - var scheduledTootUid: String? = null, + var scheduledTootId: String? = null, var savedTootUid: Int? = null, + var draftId: Int? = null, var tootText: String? = null, var mediaUrls: List? = null, var mediaDescriptions: List? = null, @@ -1133,6 +1126,7 @@ class ComposeActivity : BaseActivity(), var replyingStatusAuthor: String? = null, var replyingStatusContent: String? = null, var mediaAttachments: List? = null, + var draftAttachments: List? = null, var scheduledAt: String? = null, var sensitive: Boolean? = null, var poll: NewPoll? = null, @@ -1166,7 +1160,6 @@ class ComposeActivity : BaseActivity(), } } - @JvmStatic fun canHandleMimeType(mimeType: String?): Boolean { return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java index ebc292ab4..b2fa94c34 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.adapter; +package com.keylesspalace.tusky.components.compose; import android.content.Context; import android.preference.PreferenceManager; @@ -53,11 +53,15 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter private final ArrayList resultList; private final AutocompletionProvider autocompletionProvider; + private final boolean animateAvatar; + private final boolean animateEmojis; - public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { + public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) { super(); resultList = new ArrayList<>(); this.autocompletionProvider = autocompletionProvider; + this.animateAvatar = animateAvatar; + this.animateEmojis = animateEmojis; } @Override @@ -147,15 +151,12 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter ); accountViewHolder.username.setText(formattedUsername); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), - account.getEmojis(), accountViewHolder.displayName); + account.getEmojis(), accountViewHolder.displayName, animateEmojis); accountViewHolder.displayName.setText(emojifiedName); int avatarRadius = accountViewHolder.avatar.getContext().getResources() .getDimensionPixelSize(R.dimen.avatar_radius_42dp); - boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext()) - .getBoolean("animateGifAvatars", false); - ImageLoadingHelper.loadAvatar( account.getAvatar(), accountViewHolder.avatar, 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 74d276318..ad5debb7b 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 @@ -21,8 +21,8 @@ import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase @@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles import java.util.* import javax.inject.Inject -/** - * Throw when trying to add an image when video is already present or the other way around - */ -class VideoOrImageException : Exception() - - -class ComposeViewModel -@Inject constructor( +class ComposeViewModel @Inject constructor( private val api: MastodonApi, private val accountManager: AccountManager, private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, + private val draftHelper: DraftHelper, private val saveTootHelper: SaveTootHelper, private val db: AppDatabase ) : RxAwareViewModel() { @@ -59,7 +53,8 @@ class ComposeViewModel private var replyingStatusContent: String? = null internal var startingText: String? = null private var savedTootUid: Int = 0 - private var scheduledTootUid: String? = null + private var draftId: Int = 0 + private var scheduledTootId: String? = null private var startingContentWarning: String = "" private var inReplyToId: String? = null private var quoteId: String? = null @@ -84,10 +79,6 @@ class ComposeViewModel val markMediaAsSensitive = mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) - fun toggleMarkSensitive() { - this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!! - } - val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val showContentWarning = mutableLiveData(false) val setupComplete = mutableLiveData(false) @@ -101,7 +92,7 @@ class ComposeViewModel private val mediaToDisposable = mutableMapOf() - private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() + private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() fun loadInstanceDataFromNetwork() { @@ -143,7 +134,7 @@ class ComposeViewModel .autoDispose() } - fun pickMedia(uri: Uri): LiveData> { + fun pickMedia(uri: Uri, description: String? = null): LiveData> { // We are not calling .toLiveData() here because we don't want to stop the process when // the Activity goes away temporarily (like on screen rotation). val liveData = MutableLiveData>() @@ -155,7 +146,7 @@ class ComposeViewModel && mediaItems[0].type == QueuedMedia.Type.IMAGE) { throw VideoOrImageException() } else { - addMediaToQueue(type, uri, size) + addMediaToQueue(type, uri, size, description) } } .subscribe({ queuedMedia -> @@ -167,12 +158,23 @@ class ComposeViewModel return liveData } - private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia { - val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize) + private fun addMediaToQueue( + type: QueuedMedia.Type, + uri: Uri, + mediaSize: Long, + description: String? = null + ): QueuedMedia { + val mediaItem = QueuedMedia( + localId = System.currentTimeMillis(), + uri = uri, + type = type, + mediaSize = mediaSize, + description = description + ) media.value = media.value!! + mediaItem mediaToDisposable[mediaItem.localId] = mediaUploader .uploadMedia(mediaItem) - .subscribe ({ event -> + .subscribe({ event -> val item = media.value?.find { it.localId == mediaItem.localId } ?: return@subscribe val newMediaItem = when (event) { @@ -207,6 +209,10 @@ class ComposeViewModel media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } } + fun toggleMarkSensitive() { + this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true + } + fun didChange(content: String?, contentWarning: String?): Boolean { val textChanged = !(content.isNullOrEmpty() @@ -227,29 +233,37 @@ class ComposeViewModel } fun deleteDraft() { - saveTootHelper.deleteDraft(this.savedTootUid) + if (savedTootUid != 0) { + saveTootHelper.deleteDraft(savedTootUid) + } + if (draftId != 0) { + draftHelper.deleteDraftAndAttachments(draftId) + .subscribe() + } } fun saveDraft(content: String, contentWarning: String) { - val mediaUris = mutableListOf() - val mediaDescriptions = mutableListOf() - for (item in media.value!!) { + + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + media.value?.forEach { item -> mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) } - saveTootHelper.saveToot( - content, - contentWarning, - null, - mediaUris, - mediaDescriptions, - savedTootUid, - inReplyToId, - replyingStatusContent, - replyingStatusAuthor, - statusVisibility.value!!, - poll.value - ) + + draftHelper.saveDraft( + draftId = draftId, + accountId = accountManager.activeAccount?.id!!, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = markMediaAsSensitive.value!!, + visibility = statusVisibility.value!!, + mediaUris = mediaUris, + mediaDescriptions = mediaDescriptions, + poll = poll.value, + failedToSend = false + ).subscribe() } /** @@ -263,7 +277,7 @@ class ComposeViewModel ): LiveData { val deletionObservable = if (isEditingScheduledToot) { - api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } + api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } } else { just(Unit) }.toLiveData() @@ -281,22 +295,22 @@ class ComposeViewModel } val tootToSend = TootToSend( - content, - spoilerText, - statusVisibility.value!!.serverString(), - mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), - mediaIds, - mediaUris.map { it.toString() }, - mediaDescriptions, + text = content, + warningText = spoilerText, + visibility = statusVisibility.value!!.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + mediaIds = mediaIds, + mediaUris = mediaUris.map { it.toString() }, + mediaDescriptions = mediaDescriptions, scheduledAt = scheduledAt.value, inReplyToId = inReplyToId, poll = poll.value, replyingStatusContent = null, replyingStatusAuthorUsername = null, - savedJsonUrls = null, quoteId = quoteId, accountId = accountManager.activeAccount!!.id, - savedTootUid = 0, + savedTootUid = savedTootUid, + draftId = draftId, idempotencyKey = randomAlphanumericString(16), retries = 0 ) @@ -304,9 +318,7 @@ class ComposeViewModel serviceClient.sendToot(tootToSend) } - return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } - - + return combineLiveData(deletionObservable, sendObservable) { _, _ -> } } fun updateDescription(localId: Long, description: String): LiveData { @@ -337,7 +349,6 @@ class ComposeViewModel return completedCaptioningLiveData } - fun searchAutocompleteSuggestions(token: String): List { when (token[0]) { '@' -> { @@ -388,14 +399,12 @@ class ComposeViewModel } } - override fun onCleared() { - for (uploadDisposable in mediaToDisposable.values) { - uploadDisposable.dispose() - } - super.onCleared() - } - fun setup(composeOptions: ComposeActivity.ComposeOptions?) { + + if (setupComplete.value == true) { + return + } + val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN @@ -403,6 +412,7 @@ class ComposeViewModel preferredVisibility.num.coerceAtLeast(replyVisibility.num)) inReplyToId = composeOptions?.inReplyToId + modifiedInitialState = composeOptions?.modifiedInitialState == true quoteId = composeOptions?.quoteId @@ -418,10 +428,11 @@ class ComposeViewModel } // recreate media list - // when coming from SavedTootActivity val loadedDraftMediaUris = composeOptions?.mediaUrls val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions + val draftAttachments = composeOptions?.draftAttachments if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { + // when coming from SavedTootActivity loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) .forEach { (uri, description) -> pickMedia(uri.toUri()).observeForever { errorOrItem -> @@ -430,23 +441,24 @@ class ComposeViewModel } } } + } else if (draftAttachments != null) { + // when coming from DraftActivity + draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } } else composeOptions?.mediaAttachments?.forEach { a -> - // when coming from redraft + // when coming from redraft or ScheduledTootActivity val mediaType = when (a.type) { Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO - else -> QueuedMedia.Type.IMAGE } addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) } - savedTootUid = composeOptions?.savedTootUid ?: 0 - scheduledTootUid = composeOptions?.scheduledTootUid + draftId = composeOptions?.draftId ?: 0 + scheduledTootId = composeOptions?.scheduledTootId startingText = composeOptions?.tootText - val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { startingVisibility = tootVisibility @@ -466,7 +478,6 @@ class ComposeViewModel } startingText = builder.toString() - scheduledAt.value = composeOptions?.scheduledAt composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } @@ -487,6 +498,13 @@ class ComposeViewModel scheduledAt.value = newScheduledAt } + override fun onCleared() { + for (uploadDisposable in mediaToDisposable.values) { + uploadDisposable.dispose() + } + super.onCleared() + } + private companion object { const val TAG = "ComposeViewModel" } @@ -509,3 +527,8 @@ data class ComposeInstanceParams( val pollMaxLength: Int, val supportsScheduled: Boolean ) + +/** + * Thrown when trying to add an image when video is already present or the other way around + */ +class VideoOrImageException : Exception() 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 106245d09..965f3d30a 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 @@ -173,7 +173,13 @@ class MediaUploaderImpl( val body = MultipartBody.Part.createFormData("file", filename, fileBody) - val uploadDisposable = mastodonApi.uploadMedia(body) + val description = if (media.description != null) { + MultipartBody.Part.createFormData("description", media.description) + } else { + null + } + + val uploadDisposable = mastodonApi.uploadMedia(body, description) .subscribe({ attachment -> if (media.uri.scheme == "file") { media.uri.path?.let { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java index a1a99a7dc..14c574a15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.compose.view; import android.content.Context; import android.graphics.drawable.Drawable; -import android.os.Bundle; import android.util.AttributeSet; import android.widget.Button; import android.widget.TextView; @@ -31,8 +30,9 @@ import androidx.core.content.ContextCompat; import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.MaterialDatePicker; +import com.google.android.material.timepicker.MaterialTimePicker; +import com.google.android.material.timepicker.TimeFormat; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.fragment.TimePickerFragment; import java.text.DateFormat; import java.text.ParseException; @@ -44,6 +44,12 @@ import java.util.TimeZone; public class ComposeScheduleView extends ConstraintLayout { + public interface OnTimeSetListener { + void onTimeSet(String time); + } + + private OnTimeSetListener listener; + private DateFormat dateFormat; private DateFormat timeFormat; private SimpleDateFormat iso8601; @@ -92,6 +98,10 @@ public class ComposeScheduleView extends ConstraintLayout { setEditIcons(); } + public void setListener(OnTimeSetListener listener) { + this.listener = listener; + } + private void setScheduledDateTime() { if (scheduleDateTime == null) { scheduledDateTimeView.setText(""); @@ -144,13 +154,20 @@ public class ComposeScheduleView extends ConstraintLayout { } private void openPickTimeDialog() { - TimePickerFragment picker = new TimePickerFragment(); + MaterialTimePicker.Builder pickerBuilder = new MaterialTimePicker.Builder(); if (scheduleDateTime != null) { - Bundle args = new Bundle(); - args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); - args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); - picker.setArguments(args); + pickerBuilder.setHour(scheduleDateTime.get(Calendar.HOUR_OF_DAY)) + .setMinute(scheduleDateTime.get(Calendar.MINUTE)); } + if (android.text.format.DateFormat.is24HourFormat(this.getContext())) { + pickerBuilder.setTimeFormat(TimeFormat.CLOCK_24H); + } else { + pickerBuilder.setTimeFormat(TimeFormat.CLOCK_12H); + } + + MaterialTimePicker picker = pickerBuilder.build(); + picker.addOnPositiveButtonClickListener(v -> onTimeSet(picker.getHour(), picker.getMinute())); + picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); } @@ -200,11 +217,14 @@ public class ComposeScheduleView extends ConstraintLayout { openPickTimeDialog(); } - public void onTimeSet(int hourOfDay, int minute) { + private void onTimeSet(int hourOfDay, int minute) { initializeSuggestedTime(); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.MINUTE, minute); setScheduledDateTime(); + if (listener != null) { + listener.onTimeSet(getTime()); + } } public String getTime() { 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 888e1019e..38a92358f 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 @@ -75,7 +75,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); - setDisplayName(account.getDisplayName(), account.getEmojis()); + setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setIsReply(status.getInReplyToId() != null); @@ -83,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setBookmarked(status.getBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), statusDisplayOptions.useBlurhash()); 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 0c5ff2b3a..3dc3a1e99 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 @@ -69,6 +69,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res cardViewMode = CardViewMode.NONE, confirmReblogs = preferences.getBoolean("confirmReblogs", true), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt new file mode 100644 index 000000000..5038ac00c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -0,0 +1,175 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.net.Uri +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftAttachment +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.IOUtils +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.File +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +class DraftHelper @Inject constructor( + val context: Context, + db: AppDatabase +) { + + private val draftDao = db.draftDao() + + fun saveDraft( + draftId: Int, + accountId: Long, + inReplyToId: String?, + content: String?, + contentWarning: String?, + sensitive: Boolean, + visibility: Status.Visibility, + mediaUris: List, + mediaDescriptions: List, + poll: NewPoll?, + failedToSend: Boolean + ): Completable { + return Single.fromCallable { + + val externalFilesDir = context.getExternalFilesDir("Tusky") + + if (externalFilesDir == null || !(externalFilesDir.exists())) { + Log.e("DraftHelper", "Error obtaining directory to save media.") + throw Exception() + } + + val draftDirectory = File(externalFilesDir, "Drafts") + + if (!draftDirectory.exists()) { + draftDirectory.mkdir() + } + + val uris = mediaUris.map { uriString -> + uriString.toUri() + }.map { uri -> + if (uri.isNotInFolder(draftDirectory)) { + uri.copyToFolder(draftDirectory) + } else { + uri + } + } + + val types = uris.map { uri -> + val mimeType = context.contentResolver.getType(uri) + when (mimeType?.substring(0, mimeType.indexOf('/'))) { + "video" -> DraftAttachment.Type.VIDEO + "image" -> DraftAttachment.Type.IMAGE + "audio" -> DraftAttachment.Type.AUDIO + else -> throw IllegalStateException("unknown media type") + } + } + + val attachments: MutableList = mutableListOf() + for (i in mediaUris.indices) { + attachments.add( + DraftAttachment( + uriString = uris[i].toString(), + description = mediaDescriptions[i], + type = types[i] + ) + ) + } + + DraftEntity( + id = draftId, + accountId = accountId, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = sensitive, + visibility = visibility, + attachments = attachments, + poll = poll, + failedToSend = failedToSend + ) + + }.flatMapCompletable { draft -> + draftDao.insertOrReplace(draft) + }.subscribeOn(Schedulers.io()) + } + + fun deleteDraftAndAttachments(draftId: Int): Completable { + return draftDao.find(draftId) + .flatMapCompletable { draft -> + deleteDraftAndAttachments(draft) + } + } + + fun deleteDraftAndAttachments(draft: DraftEntity): Completable { + return deleteAttachments(draft) + .andThen(draftDao.delete(draft.id)) + } + + fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) { + draftDao.loadDraftsSingle(accountId) + .flatMapObservable { Observable.fromIterable(it) } + .flatMapCompletable { draft -> + deleteDraftAndAttachments(draft) + }.subscribeOn(Schedulers.io()) + .subscribe() + } + + fun deleteAttachments(draft: DraftEntity): Completable { + return Completable.fromCallable { + draft.attachments.forEach { attachment -> + if (context.contentResolver.delete(attachment.uri, null, null) == 0) { + Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") + } + } + }.subscribeOn(Schedulers.io()) + } + + private fun Uri.isNotInFolder(folder: File): Boolean { + val filePath = path ?: return true + return File(filePath).parentFile == folder + } + + private fun Uri.copyToFolder(folder: File): Uri { + val contentResolver = context.contentResolver + + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + + val mimeType = contentResolver.getType(this) + val map = MimeTypeMap.getSingleton() + val fileExtension = map.getExtensionFromMimeType(mimeType) + + val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension) + val file = File(folder, filename) + IOUtils.copyToFile(contentResolver, this, file) + return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt new file mode 100644 index 000000000..69403fdb5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -0,0 +1,81 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.DraftAttachment + +class DraftMediaAdapter( + private val attachmentClick: () -> Unit +) : ListAdapter( + object: DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { + return DraftMediaViewHolder(AppCompatImageView(parent.context)) + } + + override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) { + getItem(position)?.let { attachment -> + if (attachment.type == DraftAttachment.Type.AUDIO) { + holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else { + Glide.with(holder.itemView.context) + .load(attachment.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.imageView) + } + } + } + + inner class DraftMediaViewHolder(val imageView: ImageView) + : RecyclerView.ViewHolder(imageView) { + init { + val thumbnailViewSize = + imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + imageView.layoutParams = layoutParams + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + imageView.setOnClickListener { + attachmentClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt new file mode 100644 index 000000000..ddf8a8385 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -0,0 +1,197 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.SavedTootActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityDraftsBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import retrofit2.HttpException +import javax.inject.Inject + +class DraftsActivity : BaseActivity(), DraftActionListener { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: DraftsViewModel by viewModels { viewModelFactory } + + private lateinit var binding: ActivityDraftsBinding + private lateinit var bottomSheet: BottomSheetBehavior + + private var oldDraftsButton: MenuItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + + binding = ActivityDraftsBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_drafts) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status) + + val adapter = DraftsAdapter(this) + + binding.draftsRecyclerView.adapter = adapter + binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) + + viewModel.drafts.observe(this) { draftList -> + if (draftList.isEmpty()) { + binding.draftsRecyclerView.hide() + binding.draftsErrorMessageView.show() + } else { + binding.draftsRecyclerView.show() + binding.draftsErrorMessageView.hide() + adapter.submitList(draftList) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.drafts, menu) + oldDraftsButton = menu.findItem(R.id.action_old_drafts) + viewModel.showOldDraftsButton() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { showOldDraftsButton -> + oldDraftsButton?.isVisible = showOldDraftsButton + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.action_old_drafts -> { + val intent = Intent(this, SavedTootActivity::class.java) + startActivityWithSlideInAnimation(intent) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onOpenDraft(draft: DraftEntity) { + + if (draft.inReplyToId != null) { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.getToot(draft.inReplyToId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe({ status -> + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility + ) + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + + }, { throwable -> + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + Log.w(TAG, "failed loading reply information", throwable) + + 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() + openDraftWithoutReply(draft) + } else { + Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) + .show() + } + }) + } else { + openDraftWithoutReply(draft) + } + } + + private fun openDraftWithoutReply(draft: DraftEntity) { + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility + ) + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + } + + override fun onDeleteDraft(draft: DraftEntity) { + viewModel.deleteDraft(draft) + Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + viewModel.restoreDraft(draft) + } + .show() + } + + companion object { + const val TAG = "DraftsActivity" + + fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt new file mode 100644 index 000000000..5dfbceac8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -0,0 +1,92 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemDraftBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.util.BindingViewHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible + +interface DraftActionListener { + fun onOpenDraft(draft: DraftEntity) + fun onDeleteDraft(draft: DraftEntity) +} + +class DraftsAdapter( + private val listener: DraftActionListener +) : PagedListAdapter>( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + + val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + val viewHolder = BindingViewHolder(binding) + + binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) + binding.draftMediaPreview.adapter = DraftMediaAdapter { + getItem(viewHolder.adapterPosition)?.let { draft -> + listener.onOpenDraft(draft) + } + } + + return viewHolder + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + getItem(position)?.let { draft -> + holder.binding.root.setOnClickListener { + listener.onOpenDraft(draft) + } + holder.binding.deleteButton.setOnClickListener { + listener.onDeleteDraft(draft) + } + holder.binding.draftSendingInfo.visible(draft.failedToSend) + + holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty()) + holder.binding.contentWarning.text = draft.contentWarning + holder.binding.content.text = draft.content + + holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty()) + (holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments) + + if (draft.poll != null) { + holder.binding.draftPoll.show() + holder.binding.draftPoll.setPoll(draft.poll) + } else { + holder.binding.draftPoll.hide() + } + } + + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..f928b6d03 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -0,0 +1,69 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import androidx.lifecycle.ViewModel +import androidx.paging.toLiveData +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.Observable +import io.reactivex.Single +import javax.inject.Inject + +class DraftsViewModel @Inject constructor( + val database: AppDatabase, + val accountManager: AccountManager, + val api: MastodonApi, + val draftHelper: DraftHelper +) : ViewModel() { + + val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20) + + private val deletedDrafts: MutableList = mutableListOf() + + fun showOldDraftsButton(): Observable { + return database.tootDao().savedTootCount() + .map { count -> count > 0 } + } + + fun deleteDraft(draft: DraftEntity) { + // this does not immediately delete media files to avoid unnecessary file operations + // in case the user decides to restore the draft + database.draftDao().delete(draft.id) + .subscribe() + deletedDrafts.add(draft) + } + + fun restoreDraft(draft: DraftEntity) { + database.draftDao().insertOrReplace(draft) + .subscribe() + deletedDrafts.remove(draft) + } + + fun getToot(tootId: String): Single { + return api.status(tootId) + } + + override fun onCleared() { + deletedDrafts.forEach { + draftHelper.deleteAttachments(it).subscribe() + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt index f4505ad69..ca04f9c70 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -1,7 +1,6 @@ package com.keylesspalace.tusky.components.instancemute import android.os.Bundle -import android.view.MenuItem import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment @@ -32,16 +31,6 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector { .commit() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = androidInjector } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index bb850bdcf..093fc42d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -5,6 +5,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -14,7 +15,6 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.fragment.BaseFragment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide @@ -30,7 +30,7 @@ import retrofit2.Response import java.io.IOException import javax.inject.Inject -class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { +class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { @Inject lateinit var api: MastodonApi @@ -39,10 +39,6 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { private var adapter = DomainMutesAdapter(this) private lateinit var scrollListener: EndlessOnScrollListener - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_instance_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) 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 4b5d4c03a..4aefab7ed 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 @@ -20,8 +20,8 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.util.Log -import android.view.MenuItem import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import androidx.preference.PreferenceManager import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.MainActivity @@ -59,33 +59,36 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference setDisplayShowHomeEnabled(true) } - val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { - GENERAL_PREFERENCES -> { - setTitle(R.string.action_view_preferences) - PreferencesFragment.newInstance() - } - ACCOUNT_PREFERENCES -> { - setTitle(R.string.action_view_account_preferences) - AccountPreferencesFragment.newInstance() - } - NOTIFICATION_PREFERENCES -> { - setTitle(R.string.pref_title_edit_notification_settings) - NotificationPreferencesFragment.newInstance() - } - TAB_FILTER_PREFERENCES -> { - setTitle(R.string.pref_title_status_tabs) - TabFilterPreferencesFragment.newInstance() - } - PROXY_PREFERENCES -> { - setTitle(R.string.pref_title_http_proxy_settings) - ProxyPreferencesFragment.newInstance() - } - else -> throw IllegalArgumentException("preferenceType not known") - } + val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE" - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit() + val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) + ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { + GENERAL_PREFERENCES -> { + setTitle(R.string.action_view_preferences) + PreferencesFragment.newInstance() + } + ACCOUNT_PREFERENCES -> { + setTitle(R.string.action_view_account_preferences) + AccountPreferencesFragment.newInstance() + } + NOTIFICATION_PREFERENCES -> { + setTitle(R.string.pref_title_edit_notification_settings) + NotificationPreferencesFragment.newInstance() + } + TAB_FILTER_PREFERENCES -> { + setTitle(R.string.pref_title_status_tabs) + TabFilterPreferencesFragment.newInstance() + } + PROXY_PREFERENCES -> { + setTitle(R.string.pref_title_http_proxy_settings) + ProxyPreferencesFragment.newInstance() + } + else -> throw IllegalArgumentException("preferenceType not known") + } + + supportFragmentManager.commit { + replace(R.id.fragment_container, fragment, fragmentTag) + } restartActivitiesOnExit = intent.getBooleanExtra("restart", false) @@ -101,16 +104,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun saveInstanceState(outState: Bundle) { outState.putBoolean("restart", restartActivitiesOnExit) } 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 609966161..a2035fcbd 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 @@ -171,6 +171,13 @@ 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_limited_bandwidth_settings) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 3ecadd589..2c7f2d464 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.report import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.activity.viewModels import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R @@ -30,7 +29,6 @@ import kotlinx.android.synthetic.main.activity_report.* import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject - class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @Inject @@ -120,16 +118,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { wizard.currentItem = 0 } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - closeScreen() - return true - } - } - return super.onOptionsItemSelected(item) - } - companion object { private const val ACCOUNT_ID = "account_id" private const val ACCOUNT_USERNAME = "account_username" 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 2c2b70067..a177ee2cf 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 @@ -75,7 +75,7 @@ class StatusViewHolder( sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), mediaViewHeight) - statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime) + statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) setCreatedAt(status.createdAt) } @@ -90,7 +90,7 @@ class StatusViewHolder( itemView.statusContentWarningButton.hide() itemView.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription) + val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis) itemView.statusContentWarningDescription.text = emojiSpoiler itemView.statusContentWarningDescription.show() itemView.statusContentWarningButton.show() @@ -126,7 +126,7 @@ class StatusViewHolder( listener: LinkListener, removeQuote: Boolean) { if (expanded) { - val emojifiedText = content.emojify(emojis, itemView.statusContent) + val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis) LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) } else { LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) 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 e9026fa28..397e63d2a 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 @@ -113,6 +113,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje cardViewMode = CardViewMode.NONE, confirmReblogs = preferences.getBoolean("confirmReblogs", true), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 18b04df52..40a67a4eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -104,23 +103,13 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun refreshStatuses() { viewModel.reload() } override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( - scheduledTootUid = item.id, + scheduledTootId = item.id, tootText = item.params.text, contentWarning = item.params.spoilerText, mediaAttachments = item.mediaAttachments, 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 16bcac75b..2135d928c 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 @@ -20,7 +20,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu -import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.widget.SearchView import com.google.android.material.tabs.TabLayoutMediator @@ -83,17 +82,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { return true } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - - private fun getPageTitle(position: Int): CharSequence? { + private fun getPageTitle(position: Int): CharSequence { return when (position) { 0 -> getString(R.string.title_statuses) 1 -> getString(R.string.title_accounts) 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 0a551fb70..97d250311 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 @@ -245,8 +245,8 @@ class SearchViewModel @Inject constructor( return accountManager.getAllAccountsOrderedByActive() } - fun muteAccount(accountId: String, notifications: Boolean) { - timelineCases.mute(accountId, notifications) + fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { + timelineCases.mute(accountId, notifications, duration) } fun pinAccount(status: Status, isPin: Boolean) { 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 c135ad7d6..b6bc95681 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 @@ -25,7 +25,7 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.LinkListener -class SearchAccountsAdapter(private val linkListener: LinkListener) +class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) : PagedListAdapter(ACCOUNT_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -37,7 +37,7 @@ class SearchAccountsAdapter(private val linkListener: LinkListener) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { getItem(position)?.let { item -> (holder as AccountViewHolder).apply { - setupWithAccount(item) + setupWithAccount(item, animateAvatars, animateEmojis) setupLinkListener(linkListener) } } 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 714580f7c..c453f97c5 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 @@ -18,12 +18,23 @@ package com.keylesspalace.tusky.components.search.fragments import androidx.lifecycle.LiveData import androidx.paging.PagedList import androidx.paging.PagedListAdapter +import androidx.preference.PreferenceManager import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.NetworkState +import kotlinx.android.synthetic.main.fragment_search.* class SearchAccountsFragment : SearchFragment() { - override fun createAdapter(): PagedListAdapter = SearchAccountsAdapter(this) + override fun createAdapter(): PagedListAdapter { + val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + + return SearchAccountsAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + } override val networkStateRefresh: LiveData get() = viewModel.networkStateAccountRefresh diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchNotestockFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchNotestockFragment.kt index ce450ad96..e28b5e9ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchNotestockFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchNotestockFragment.kt @@ -72,6 +72,7 @@ class SearchNotestockFragment : SearchFragment viewModel.muteAccount(accountId, notifications) } - ) + ) { notifications, duration -> + viewModel.muteAccount(accountId, notifications, duration) + } } private fun accountIsInMentions(account: AccountEntity?, mentions: Array): Boolean { 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 0d808cd40..0cdc38439 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 @@ -54,6 +54,7 @@ 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.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.view.showMuteAccountDialog @@ -87,6 +88,7 @@ class SearchStatusesFragment : SearchFragment { + LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context) } } @@ -404,8 +407,8 @@ class SearchStatusesFragment : SearchFragment - viewModel.muteAccount(accountId, notifications) + ) { notifications, duration -> + viewModel.muteAccount(accountId, notifications, duration) } } 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 5c5b7cb62..d35fd3891 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -28,9 +28,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationEntity; * DB version & declare DAO */ -@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, +@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 24) + }, version = 25) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); @@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase { public abstract InstanceDao instanceDao(); public abstract ConversationsDao conversationDao(); public abstract TimelineDao timelineDao(); + public abstract DraftDao draftDao(); public static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override @@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); database.execSQL("DROP TABLE TootEntity;"); database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); - } }; @@ -347,4 +347,22 @@ public abstract class AppDatabase extends RoomDatabase { } }; + public static final Migration MIGRATION_24_25 = new Migration(24, 25) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + + "`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)" + ); + } + }; } 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 fb625b84a..3faf4791e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -25,10 +25,7 @@ import com.keylesspalace.tusky.STREAMING import com.keylesspalace.tusky.TabData 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.Poll -import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder @@ -155,4 +152,23 @@ class Converters { return gson.fromJson(pollJson, Poll::class.java) } -} \ No newline at end of file + @TypeConverter + fun newPollToJson(newPoll: NewPoll?): String? { + return gson.toJson(newPoll) + } + + @TypeConverter + fun jsonToNewPoll(newPollJson: String?): NewPoll? { + return gson.fromJson(newPollJson, NewPoll::class.java) + } + + @TypeConverter + fun draftAttachmentListToJson(draftAttachments: List?): String? { + return gson.toJson(draftAttachments) + } + + @TypeConverter + fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List? { + return gson.fromJson(draftAttachmentListJson, object : TypeToken>() {}.type) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt new file mode 100644 index 000000000..065af1aed --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -0,0 +1,44 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Completable +import io.reactivex.Single + +@Dao +interface DraftDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(draft: DraftEntity): Completable + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") + fun loadDrafts(accountId: Long): DataSource.Factory + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") + fun loadDraftsSingle(accountId: Long): Single> + + @Query("DELETE FROM DraftEntity WHERE id = :id") + fun delete(id: Int): Completable + + @Query("SELECT * FROM DraftEntity WHERE id = :id") + fun find(id: Int): Single + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt new file mode 100644 index 000000000..be1eca589 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -0,0 +1,55 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import kotlinx.android.parcel.Parcelize + +@Entity +@TypeConverters(Converters::class) +data class DraftEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val accountId: Long, + val inReplyToId: String?, + val content: String?, + val contentWarning: String?, + val sensitive: Boolean, + val visibility: Status.Visibility, + val attachments: List, + val poll: NewPoll?, + val failedToSend: Boolean +) + +@Parcelize +data class DraftAttachment( + val uriString: String, + val description: String?, + val type: Type +): Parcelable { + val uri: Uri + get() = uriString.toUri() + + enum class Type { + IMAGE, VIDEO, AUDIO; + } +} 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 da98cb4b3..296111d30 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). indices = [Index("authorServerId", "timelineUserId")] ) -@TypeConverters(TootEntity.Converters::class) +@TypeConverters(Converters::class) data class TimelineStatusEntity( val serverId: String, // id never flips: we need it for sorting so it's a real id val url: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java index c121e1705..f46c2753a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java @@ -16,12 +16,12 @@ package com.keylesspalace.tusky.db; import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; +import io.reactivex.Observable; + /** * Created by cto3543 on 28/06/2017. * @@ -30,8 +30,6 @@ import java.util.List; @Dao public interface TootDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insertOrReplace(TootEntity users); @Query("SELECT * FROM TootEntity ORDER BY uid DESC") List loadAll(); @@ -41,4 +39,7 @@ public interface TootDao { @Query("SELECT * FROM TootEntity WHERE uid = :uid") TootEntity find(int uid); -} + + @Query("SELECT COUNT(*) FROM TootEntity") + Observable savedTootCount(); +} \ No newline at end of file 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 907c92726..69dfa2c09 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di import com.keylesspalace.tusky.* 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.preference.PreferencesActivity import com.keylesspalace.tusky.components.report.ReportActivity @@ -109,6 +110,9 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity + @ContributesAndroidInjector + abstract fun contributesDraftActivity(): DraftsActivity + @ContributesAndroidInjector abstract fun contributesAccessTokenLoginActivity(): AccessTokenLoginActivity } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 7e86bbac5..1137b12b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -80,7 +80,7 @@ class AppModule { AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, - AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24) + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 588ae3ca9..155847b46 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -16,6 +16,8 @@ package com.keylesspalace.tusky.di import android.content.Context +import android.content.SharedPreferences +import android.os.Build import android.text.Spanned import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -25,16 +27,21 @@ import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.NotestockApi -import com.keylesspalace.tusky.util.okhttpClient +import com.keylesspalace.tusky.util.getNonNullString import dagger.Module import dagger.Provides import net.accelf.yuito.HttpToastInterceptor +import okhttp3.Cache +import okhttp3.OkHttp import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit import javax.inject.Singleton /** @@ -56,9 +63,37 @@ class NetworkModule { @Singleton fun providesHttpClient( accountManager: AccountManager, - context: Context + context: Context, + preferences: SharedPreferences ): OkHttpClient { - return okhttpClient(context) + val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) + val httpServer = preferences.getNonNullString("httpProxyServer", "") + val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 + val cacheSize = 25 * 1024 * 1024L // 25 MiB + val builder = OkHttpClient.Builder() + .addInterceptor { chain -> + /** + * Add a custom User-Agent that contains Tusky, Android and OkHttp Version to all requests + * Example: + * User-Agent: Tusky/1.1.2 Android/5.0.2 OkHttp/4.9.0 + * */ + val requestWithUserAgent = chain.request().newBuilder() + .header( + "User-Agent", + "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" + ) + .build() + chain.proceed(requestWithUserAgent) + } + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cache(Cache(context.cacheDir, cacheSize)) + + if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { + val address = InetSocketAddress.createUnresolved(httpServer, httpPort) + builder.proxy(Proxy(Proxy.Type.HTTP, address)) + } + return builder .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { @@ -89,18 +124,10 @@ class NetworkModule { @Provides @Singleton - fun providesNotestockApi(context: Context, + fun providesNotestockApi(okHttpClient: OkHttpClient, gson: Gson): NotestockApi { - val httpClient = okhttpClient(context) - .apply { - if (BuildConfig.DEBUG) { - addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) - addInterceptor(HttpToastInterceptor(context)) - } - } - .build() val retrofit = Retrofit.Builder().baseUrl("https://notestock.osa-p.net") - .client(httpClient) + .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) .build() 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 e8025d638..bed2c9833 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel 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.search.SearchViewModel @@ -92,6 +93,11 @@ abstract class ViewModelModule { @ViewModelKey(AnnouncementsViewModel::class) internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(DraftsViewModel::class) + internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(QuickTootViewModel::class) 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 d5785ae36..7b202713e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -17,10 +17,10 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -36,6 +36,7 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show @@ -45,14 +46,12 @@ import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_account_list.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException import java.util.* import javax.inject.Inject -class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { +class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable { @Inject lateinit var api: MastodonApi @@ -71,10 +70,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { id = arguments?.getString(ARG_ID) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_account_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -85,11 +80,15 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + val pm = PreferenceManager.getDefaultSharedPreferences(view.context) + val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + adapter = when (type) { - Type.BLOCKS -> BlocksAdapter(this) - Type.MUTES -> MutesAdapter(this) - Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) - else -> FollowAdapter(this) + Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis) + Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis) + Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis) + else -> FollowAdapter(this, animateAvatar, animateEmojis) } recyclerView.adapter = adapter @@ -202,27 +201,23 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) { - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onRespondToFollowRequestSuccess(position) - } else { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - val call = if (accept) { + if (accept) { api.authorizeFollowRequest(accountId) } else { api.rejectFollowRequest(accountId) - } - callList.add(call) - call.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ + onRespondToFollowRequestSuccess(position) + }, { throwable -> + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $accountId.", throwable) + }) + } private fun onRespondToFollowRequestSuccess(position: Int) { @@ -230,15 +225,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { followRequestsAdapter.removeItem(position) } - private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { - val verb = if (accept) { - "accept" - } else { - "reject" - } - Log.e(TAG, "Failed to $verb account id $accountId.") - } - private fun getFetchCallByListType(fromId: String?): Single>> { return when (type) { Type.FOLLOWS -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index ed1fbad8f..b85a87f31 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -18,12 +18,13 @@ package com.keylesspalace.tusky.fragment import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -34,14 +35,17 @@ 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.show import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.fragment_timeline.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException import java.util.* @@ -53,7 +57,7 @@ import javax.inject.Inject * Fragment with multiple columns of media previews for the specified account. */ -class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { +class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { companion object { @JvmStatic fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { @@ -77,14 +81,13 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { lateinit var api: MastodonApi private val adapter = MediaGridAdapter() - private var currentCall: Call>? = null private val statuses = mutableListOf() private var fetchingStatus = FetchingStatus.NOT_FETCHING private lateinit var accountId: String - private val callback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val callback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { @@ -106,7 +109,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { swipeRefreshLayout.isRefreshing = false @@ -127,22 +130,23 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { if (statuses.isEmpty()) { statusView.show() - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) } } } } + + override fun onSubscribe(d: Disposable) {} } - private val bottomCallback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val bottomCallback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING val body = response.body() body?.let { fetched -> @@ -159,6 +163,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } } + override fun onSubscribe(d: Disposable) { } } override fun onCreate(savedInstanceState: Bundle?) { @@ -166,10 +171,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true accountId = arguments?.getString(ACCOUNT_ID_ARG)!! } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timeline, container, false) - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -201,8 +202,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { statuses.lastOrNull()?.let { (id) -> Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)") fetchingStatus = FetchingStatus.FETCHING_BOTTOM - currentCall = api.accountStatuses(accountId, id, null, null, null, true, null) - currentCall?.enqueue(bottomCallback) + api.accountStatuses(accountId, id, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(bottomCallback) } } } @@ -215,14 +218,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { private fun refresh() { statusView.hide() if (fetchingStatus != FetchingStatus.NOT_FETCHING) return - currentCall = if (statuses.isEmpty()) { + if (statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING api.accountStatuses(accountId, null, null, null, null, true, null) } else { fetchingStatus = FetchingStatus.REFRESHING api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) - } - currentCall?.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) if (!isSwipeToRefreshEnabled) topProgressBar?.show() @@ -234,8 +238,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING - currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) - currentCall?.enqueue(callback) + api.accountStatuses(accountId, null, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) } else if (needToRefresh) refresh() @@ -260,10 +266,8 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } } Attachment.Type.UNKNOWN -> { - }/* Intentionally do nothing. This case is here is to handle when new attachment - * types are added to the API before code is added here to handle them. So, the - * best fallback is to just show the preview and ignore requests to view them. */ - + LinkHelper.openLink(items[currentIndex].attachment.url, context) + } } } @@ -340,5 +344,4 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { needToRefresh = true } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java deleted file mode 100644 index b674b8ba5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java +++ /dev/null @@ -1,43 +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.fragment; - -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import java.util.ArrayList; -import java.util.List; - -import retrofit2.Call; - -public class BaseFragment extends Fragment { - protected List callList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - callList = new ArrayList<>(); - } - - @Override - public void onDestroy() { - for (Call call : callList) { - call.cancel(); - } - super.onDestroy(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 1a0569a0d..4f2fa6510 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -105,13 +105,11 @@ import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.keylesspalace.tusky.util.StringUtils.isLessThan; import static com.uber.autodispose.AutoDispose.autoDisposable; @@ -128,8 +126,9 @@ public class NotificationsFragment extends SFragment implements private static final int LOAD_AT_ONCE = 30; private int maxPlaceholderId = 0; + private final Set notificationFilter = new HashSet<>(); - private Set notificationFilter = new HashSet<>(); + private final CompositeDisposable disposables = new CompositeDisposable(); private enum FetchEnd { TOP, @@ -257,6 +256,7 @@ public class NotificationsFragment extends SFragment implements CardViewMode.NONE, preferences.getBoolean("confirmReblogs", true), preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) ); @@ -694,32 +694,21 @@ public class NotificationsFragment extends SFragment implements updateAdapter(); //Execute clear notifications request - Call call = mastodonApi.clearNotifications(); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (isAdded()) { - if (!response.isSuccessful()) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - }); - callList.add(call); + mastodonApi.clearNotifications() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + // nothing to do + }, + throwable -> { + //Reload notifications on failure + fullyRefreshWithProgressBar(true); + }); } private void resetNotificationsLoad() { - for (Call callItem : callList) { - callItem.cancel(); - } - callList.clear(); + disposables.clear(); bottomLoading = false; topLoading = false; @@ -849,8 +838,8 @@ public class NotificationsFragment extends SFragment implements @Override public void onRespondToFollowRequest(boolean accept, String id, int position) { Single request = accept ? - mastodonApi.authorizeFollowRequestObservable(id) : - mastodonApi.rejectFollowRequestObservable(id); + mastodonApi.authorizeFollowRequest(id) : + mastodonApi.rejectFollowRequest(id); request.observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( @@ -968,27 +957,20 @@ public class NotificationsFragment extends SFragment implements bottomLoading = true; } - Call> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null); - - call.enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful()) { - String linkHeader = response.headers().get("Link"); - onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); - } else { - onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - if (!call.isCanceled()) - onFetchNotificationsFailure((Exception) t, fetchEnd, pos); - } - }); - callList.add(call); + Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + String linkHeader = response.headers().get("Link"); + onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); + } else { + onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + throwable -> onFetchNotificationsFailure(throwable, fetchEnd, pos)); + disposables.add(notificationCall); } private void onFetchNotificationsSuccess(List notifications, String linkHeader, @@ -1047,7 +1029,7 @@ public class NotificationsFragment extends SFragment implements progressBar.setVisibility(View.GONE); } - private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); @@ -1059,7 +1041,7 @@ public class NotificationsFragment extends SFragment implements this.statusView.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); this.showingError = true; - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1074,7 +1056,7 @@ public class NotificationsFragment extends SFragment implements } updateFilterVisibility(); } - Log.e(TAG, "Fetch failure: " + exception.getMessage()); + Log.e(TAG, "Fetch failure: " + throwable.getMessage()); if (fetchEnd == FetchEnd.TOP) { topLoading = false; 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 0df5e5ed3..e07edd91b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -20,7 +20,6 @@ import android.app.DownloadManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -30,8 +29,6 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -41,14 +38,14 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; -import androidx.preference.PreferenceManager; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BottomSheetActivity; import com.keylesspalace.tusky.MainActivity; -import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.PostLookupFallbackBehavior; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity; @@ -63,6 +60,7 @@ import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; +import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.viewdata.AttachmentViewData; @@ -75,9 +73,8 @@ import java.util.regex.Pattern; import javax.inject.Inject; -import kotlin.Unit; - import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -91,7 +88,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear * up what needs to be where. */ -public abstract class SFragment extends BaseFragment implements Injectable { +public abstract class SFragment extends Fragment implements Injectable { protected abstract void removeItem(int position); @@ -102,7 +99,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { private static List filters; private boolean filterRemoveRegex; private Matcher filterRemoveRegexMatcher; - private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); + private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); @Inject public MastodonApi mastodonApi; @@ -367,8 +364,8 @@ public abstract class SFragment extends BaseFragment implements Injectable { MuteAccountDialog.showMuteAccountDialog( this.getActivity(), accountUsername, - (notifications) -> { - timelineCases.mute(accountId, notifications); + (notifications, duration) -> { + timelineCases.mute(accountId, notifications, duration); return Unit.INSTANCE; } ); @@ -422,10 +419,9 @@ public abstract class SFragment extends BaseFragment implements Injectable { } break; } + default: case UNKNOWN: { - /* Intentionally do nothing. This case is here is to handle when new attachment - * types are added to the API before code is added here to handle them. So, the - * best fallback is to just show the preview and ignore requests to view them. */ + LinkHelper.openLink(active.getUrl(), getContext()); break; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java deleted file mode 100644 index 1349a59c6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright 2019 kyori19 - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment; - -import android.app.Dialog; -import android.app.TimePickerDialog; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -import com.keylesspalace.tusky.components.compose.ComposeActivity; - -import java.util.Calendar; -import java.util.TimeZone; - -public class TimePickerFragment extends DialogFragment { - - public static final String PICKER_TIME_HOUR = "picker_time_hour"; - public static final String PICKER_TIME_MINUTE = "picker_time_minute"; - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); - if (args != null) { - calendar.set(Calendar.HOUR_OF_DAY, args.getInt(PICKER_TIME_HOUR)); - calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE)); - } - - return new TimePickerDialog(getContext(), - android.R.style.Theme_DeviceDefault_Dialog, - (ComposeActivity) getActivity(), - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), - true); - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 6eadea53a..492e9ab52 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -27,11 +27,13 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; +import androidx.core.content.ContextCompat; import androidx.core.util.Pair; import androidx.core.widget.ContentLoadingProgressBar; import androidx.lifecycle.Lifecycle; @@ -105,12 +107,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import kotlin.Unit; import kotlin.collections.CollectionsKt; @@ -118,8 +122,6 @@ import kotlin.jvm.functions.Function1; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.WebSocket; -import retrofit2.Call; -import retrofit2.Callback; import retrofit2.Response; import static android.content.Context.CONNECTIVITY_SERVICE; @@ -285,6 +287,7 @@ public class TimelineFragment extends SFragment implements CardViewMode.NONE, preferences.getBoolean("confirmReblogs", true), preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) ); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); @@ -1150,7 +1153,7 @@ public class TimelineFragment extends SFragment implements } } - private Call> getFetchCallByTimelineType(String fromId, String uptoId) { + private Single>> getFetchCallByTimelineType(String fromId, String uptoId) { MastodonApi api = mastodonApi; switch (kind) { default: @@ -1197,37 +1200,31 @@ public class TimelineFragment extends SFragment implements .observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( - (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), - (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) + result -> onFetchTimelineSuccess(result, fetchEnd, pos), + err -> onFetchTimelineFailure(err, fetchEnd, pos) ); } else { - Callback> callback = new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful()) { - @Nullable - String newNextId = extractNextId(response); - if (newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId; - } - onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); - } else { - onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - onFetchTimelineFailure((Exception) t, fetchEnd, pos); - } - }; - - Call> listCall = getFetchCallByTimelineType(maxId, sinceId); - callList.add(listCall); - listCall.enqueue(callback); + getFetchCallByTimelineType(maxId, sinceId) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + @Nullable + String newNextId = extractNextId(response); + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId; + } + onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); + } else { + onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + err -> onFetchTimelineFailure(err, fetchEnd, pos) + ); } } @@ -1304,7 +1301,7 @@ public class TimelineFragment extends SFragment implements } } - private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); topProgressBar.hide(); @@ -1323,7 +1320,7 @@ public class TimelineFragment extends SFragment implements } else if (this.statuses.isEmpty()) { swipeRefreshLayout.setEnabled(false); this.statusView.setVisibility(View.VISIBLE); - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1338,7 +1335,7 @@ public class TimelineFragment extends SFragment implements } } - Log.e(TAG, "Fetch Failure: " + exception.getMessage()); + Log.e(TAG, "Fetch Failure: " + throwable.getMessage()); updateBottomLoadingState(fetchEnd); progressBar.setVisibility(View.GONE); } @@ -1681,9 +1678,21 @@ public class TimelineFragment extends SFragment implements } }; + AccessibilityManager a11yManager; + boolean talkBackWasEnabled; + @Override public void onResume() { super.onResume(); + a11yManager = Objects.requireNonNull( + ContextCompat.getSystemService(requireContext(), AccessibilityManager.class) + ); + boolean wasEnabled = this.talkBackWasEnabled; + talkBackWasEnabled = a11yManager.isEnabled(); + Log.d(TAG, "talkback was enabled: " + wasEnabled + ", now " + talkBackWasEnabled); + if (talkBackWasEnabled && !wasEnabled) { + this.adapter.notifyDataSetChanged(); + } startUpdateTimestamp(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index 86b3d09be..b25fec26f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -17,10 +17,11 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.text.TextUtils +import androidx.fragment.app.Fragment import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.entity.Attachment -abstract class ViewMediaFragment : BaseFragment() { +abstract class ViewMediaFragment : Fragment() { private var toolbarVisibiltyDisposable: Function0? = null abstract fun setupMediaView( 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 e4f8ad23e..35931009c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -56,7 +56,6 @@ import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.settings.PrefKeys; @@ -77,9 +76,6 @@ import java.util.Locale; import javax.inject.Inject; import io.reactivex.android.schedulers.AndroidSchedulers; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; @@ -141,6 +137,7 @@ public final class ViewThreadFragment extends SFragment implements CardViewMode.NONE, preferences.getBoolean("confirmReblogs", true), preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) ); adapter = new ThreadAdapter(statusDisplayOptions, this); @@ -471,49 +468,32 @@ public final class ViewThreadFragment extends SFragment implements } private void sendStatusRequest(final String id) { - Call call = mastodonApi.status(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - int position = setStatus(response.body()); - recyclerView.scrollToPosition(position); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.status(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + status -> { + int position = setStatus(status); + recyclerView.scrollToPosition(position); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } private void sendThreadRequest(final String id) { - Call call = mastodonApi.statusContext(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - StatusContext context = response.body(); - if (response.isSuccessful() && context != null) { - swipeRefreshLayout.setRefreshing(false); - setContext(context.getAncestors(), context.getDescendants()); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.statusContext(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + context -> { + swipeRefreshLayout.setRefreshing(false); + setContext(context.getAncestors(), context.getDescendants()); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } - private void onThreadRequestFailure(final String id) { + private void onThreadRequestFailure(final String id, final Throwable throwable) { View view = getView(); swipeRefreshLayout.setRefreshing(false); if (view != null) { @@ -524,7 +504,7 @@ public final class ViewThreadFragment extends SFragment implements }) .show(); } else { - Log.e(TAG, "Couldn't display thread fetch error message"); + Log.e(TAG, "Network request failed", throwable); } } 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 08a5483db..28ac77b6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -56,14 +56,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> - - @GET("api/v1/timelines/home") - fun homeTimelineSingle( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? - ): Single> + ): Single>> @GET("api/v1/timelines/public") fun publicTimeline( @@ -71,7 +64,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/tag/{hashtag}") fun hashtagTimeline( @@ -81,7 +74,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/list/{listId}") fun listTimeline( @@ -89,7 +82,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/notifications") fun notifications( @@ -97,7 +90,7 @@ interface MastodonApi { @Query("since_id") sinceId: String?, @Query("limit") limit: Int?, @Query("exclude_types[]") excludes: Set? - ): Call> + ): Single>> @GET("api/v1/markers") fun markersWithAuth( @@ -114,17 +107,13 @@ interface MastodonApi { ): Single> @POST("api/v1/notifications/clear") - fun clearNotifications(): Call - - @GET("api/v1/notifications/{id}") - fun notification( - @Path("id") notificationId: String - ): Call + fun clearNotifications(): Single @Multipart @POST("api/v1/media") fun uploadMedia( - @Part file: MultipartBody.Part + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null ): Single @FormUrlEncoded @@ -145,12 +134,12 @@ interface MastodonApi { @GET("api/v1/statuses/{id}") fun status( @Path("id") statusId: String - ): Call + ): Single @GET("api/v1/statuses/{id}/context") fun statusContext( @Path("id") statusId: String - ): Call + ): Single @GET("api/v1/statuses/{id}/reblogged_by") fun statusRebloggedBy( @@ -289,7 +278,7 @@ interface MastodonApi { @Query("exclude_replies") excludeReplies: Boolean?, @Query("only_media") onlyMedia: Boolean?, @Query("pinned") pinned: Boolean? - ): Call> + ): Single>> @GET("api/v1/accounts/{id}/followers") fun accountFollowers( @@ -330,7 +319,8 @@ interface MastodonApi { @POST("api/v1/accounts/{id}/mute") fun muteAccount( @Path("id") accountId: String, - @Field("notifications") notifications: Boolean? = null + @Field("notifications") notifications: Boolean? = null, + @Field("duration") duration: Int? = null ): Single @POST("api/v1/accounts/{id}/unmute") @@ -391,14 +381,14 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/bookmarks") fun bookmarks( @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/follow_requests") fun followRequests( @@ -408,20 +398,10 @@ interface MastodonApi { @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequest( - @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/authorize") - fun authorizeFollowRequestObservable( - @Path("id") accountId: String ): Single @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequestObservable( + fun rejectFollowRequest( @Path("id") accountId: String ): Single diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index efdb410b3..8cf2b688d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -33,7 +33,7 @@ interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single fun bookmark(status: Status, bookmark: Boolean): Single - fun mute(id: String, notifications: Boolean) + fun mute(id: String, notifications: Boolean, duration: Int) fun block(id: String) fun delete(id: String): Single fun pin(status: Status, pin: Boolean) @@ -104,8 +104,8 @@ class TimelineCasesImpl( } } - override fun mute(id: String, notifications: Boolean) { - mastodonApi.muteAccount(id, notifications) + override fun mute(id: String, notifications: Boolean, duration: Int) { + mastodonApi.muteAccount(id, notifications, duration) .subscribe({ eventHub.dispatch(MuteEvent(id)) }, { t -> 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 00ef1579d..ce689e5cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val notificationManager = NotificationManagerCompat.from(context) - if (intent.action == NotificationHelper.REPLY_ACTION) { val message = getReplyMessage(intent) @@ -89,22 +88,24 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val sendIntent = SendTootService.sendTootIntent( context, TootToSend( - text, - spoiler, - visibility.serverString(), - false, - emptyList(), - emptyList(), - emptyList(), - null, - citedStatusId, - null, - null, - null, - null, null, account.id, - 0, - randomAlphanumericString(16), - 0 + text = text, + warningText = spoiler, + visibility = visibility.serverString(), + sensitive = false, + mediaIds = emptyList(), + mediaUris = emptyList(), + mediaDescriptions = emptyList(), + scheduledAt = null, + inReplyToId = citedStatusId, + poll = null, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + quoteId = null, + accountId = account.id, + savedTootUid = -1, + draftId = -1, + idempotencyKey = randomAlphanumericString(16), + retries = 0 ) ) @@ -155,4 +156,4 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { return remoteInput.getCharSequence(NotificationHelper.KEY_REPLY, "") } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index e33c627f0..837bf9fd6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -78,9 +78,9 @@ class TimelineRepositoryImpl( sinceIdMinusOne: String?, limit: Int, accountId: Long, requestMode: TimelineRequestMode ): Single> { - return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) - .map { statuses -> - this.saveStatusesToDb(accountId, statuses, maxId, sinceId) + return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) + .map { response -> + this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId) } .flatMap { statuses -> this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) @@ -97,7 +97,7 @@ class TimelineRepositoryImpl( private fun addFromDbIfNeeded(accountId: Long, statuses: List>, maxId: String?, sinceId: String?, limit: Int, requestMode: TimelineRequestMode - ): Single>? { + ): Single> { return if (requestMode != NETWORK && statuses.size < 2) { val newMaxID = if (statuses.isEmpty()) { maxId diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index ef53ce4e8..0c7541c92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R 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.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.Injectable @@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable { lateinit var eventHub: EventHub @Inject lateinit var database: AppDatabase - + @Inject + lateinit var draftHelper: DraftHelper @Inject lateinit var saveTootHelper: SaveTootHelper @@ -164,6 +166,10 @@ class SendTootService : Service(), Injectable { if (tootToSend.savedTootUid != 0) { saveTootHelper.deleteDraft(tootToSend.savedTootUid) } + if (tootToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(tootToSend.draftId) + .subscribe() + } if (scheduled) { response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) @@ -246,17 +252,19 @@ class SendTootService : Service(), Injectable { private fun saveTootToDrafts(toot: TootToSend) { - saveTootHelper.saveToot(toot.text, - toot.warningText, - toot.savedJsonUrls, - toot.mediaUris, - toot.mediaDescriptions, - toot.savedTootUid, - toot.inReplyToId, - toot.replyingStatusContent, - toot.replyingStatusAuthorUsername, - Status.Visibility.byString(toot.visibility), - toot.poll) + 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 + ).subscribe() } private fun cancelSendingIntent(tootId: Int): PendingIntent { @@ -324,10 +332,10 @@ data class TootToSend( val poll: NewPoll?, val replyingStatusContent: String?, val replyingStatusAuthorUsername: String?, - val savedJsonUrls: List?, val quoteId: String?, val accountId: Long, val savedTootUid: Int, + val draftId: Int, val idempotencyKey: String, var retries: Int ) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index 3577f0525..9da05c33a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -31,6 +31,7 @@ object PrefKeys { const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" const val CONFIRM_REBLOGS = "confirmReblogs" const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" + const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis" const val LIMITED_BANDWIDTH_ACTIVE = "limitedBandwidthActive" const val LIMITED_BANDWIDTH_ONLY_MOBILE_NETWORK = "limitedBandwidthOnlyMobileNetwork" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt new file mode 100644 index 000000000..14aee81b5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt @@ -0,0 +1,8 @@ +package com.keylesspalace.tusky.util + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class BindingViewHolder( + val binding: T +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt index a12943330..c0da4275e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt @@ -21,20 +21,52 @@ import android.text.TextUtils import android.widget.MultiAutoCompleteTextView class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { + + private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean { + return Character.isLetterOrDigit(character) || character == '_' // simple usernames + || character == '-' // extended usernames + || character == '.' // domain dot + } + override fun findTokenStart(text: CharSequence, cursor: Int): Int { if (cursor == 0) { return cursor } var i = cursor var character = text[i - 1] - while (i > 0 && character != '@' && character != '#' && character != ':') { - // See SpanUtils.MENTION_REGEX - if (!Character.isLetterOrDigit(character) && character != '_') { + + // go up to first illegal character or character we're looking for (@, # or :) + while(i > 0 && !(character == '@' || character == '#' || character == ':')) { + if(!isMentionOrHashtagAllowedCharacter(character)) { return cursor } + i-- character = if (i == 0) ' ' else text[i - 1] } + + // maybe caught domain name? try search username + if(i > 2 && character == '@') { + var j = i - 1 + var character2 = text[i - 2] + + // again go up to first illegal character or tag "@" + while(j > 0 && character2 != '@') { + if(!isMentionOrHashtagAllowedCharacter(character2)) { + break + } + + j-- + character2 = if (j == 0) ' ' else text[j - 1] + } + + // found mention symbol, override cursor + if(character2 == '@') { + i = j + character = character2 + } + } + if (i < 1 || (character != '@' && character != '#' && character != ':') || i > 1 && !Character.isWhitespace(text[i - 2])) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index 679f38d36..7521afe40 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -16,11 +16,9 @@ @file:JvmName("CustomEmojiHelper") package com.keylesspalace.tusky.util -import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable +import android.graphics.drawable.* import android.text.SpannableStringBuilder import android.text.style.ReplacementSpan import android.view.View @@ -33,6 +31,8 @@ import com.keylesspalace.tusky.entity.Emoji import java.lang.ref.WeakReference import java.util.regex.Pattern +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.settings.PrefKeys /** * replaces emoji shortcodes in a text with EmojiSpans @@ -41,7 +41,7 @@ import java.util.regex.Pattern * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) * @return the text with the shortcodes replaced by EmojiSpans */ -fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { +fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean) : CharSequence { if(emojis.isNullOrEmpty()) return this @@ -56,9 +56,9 @@ fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { builder.setSpan(span, matcher.start(), matcher.end(), 0) Glide.with(view) - .asBitmap() + .asDrawable() .load(url) - .into(span.getTarget()) + .into(span.getTarget(animate)) } } return builder @@ -97,11 +97,29 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() } } - fun getTarget(): Target { - return object : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { + fun getTarget(animate : Boolean): Target { + return object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { viewWeakReference.get()?.let { view -> - imageDrawable = BitmapDrawable(view.context.resources, resource) + if(animate && resource is Animatable) { + val callback = resource.callback + + resource.callback = object: Drawable.Callback { + override fun unscheduleDrawable(p0: Drawable, p1: Runnable) { + callback?.unscheduleDrawable(p0, p1) + } + override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) { + callback?.scheduleDrawable(p0, p1, p2) + } + override fun invalidateDrawable(p0: Drawable) { + callback?.invalidateDrawable(p0) + view.invalidate() + } + } + resource.start() + } + + imageDrawable = resource view.invalidate() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 2249e5d13..fab45d0d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -31,6 +30,7 @@ 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; @@ -229,18 +229,20 @@ public class LinkHelper { */ 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); - CustomTabsIntent.Builder customTabsIntentBuilder = new CustomTabsIntent.Builder() + CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() .setToolbarColor(toolbarColor) - .setShowTitle(true); + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - customTabsIntentBuilder.setNavigationBarColor( - ThemeUtils.getColor(context, android.R.attr.navigationBarColor) - ); - } + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build(); - CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build(); try { customTabsIntent.launchUrl(context, uri); } catch (ActivityNotFoundException e) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt deleted file mode 100644 index 3e1b89c6a..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* Copyright 2020 Tusky Contributors - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * Lesser 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 Lesser - * General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with Tusky. If - * not, see . */ - -package com.keylesspalace.tusky.util - -import android.content.Context -import android.os.Build -import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.BuildConfig -import okhttp3.Cache -import okhttp3.OkHttp -import okhttp3.OkHttpClient -import okhttp3.tls.HandshakeCertificates -import java.io.ByteArrayInputStream -import java.net.InetSocketAddress -import java.net.Proxy -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit - -fun okhttpClient(context: Context): OkHttpClient.Builder { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - - val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) - val httpServer = preferences.getNonNullString("httpProxyServer", "") - val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 - - val cacheSize = 25 * 1024 * 1024 // 25 MiB - val builder = OkHttpClient.Builder() - .addInterceptor { chain -> - /** - * Add a custom User-Agent that contains Tusky, Android and Okhttp Version to all requests - * Example: - * User-Agent: Tusky/1.1.2 Android/5.0.2 - * */ - val requestWithUserAgent = chain.request().newBuilder() - .header( - "User-Agent", - "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" - ) - .build() - chain.proceed(requestWithUserAgent) - } - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .cache(Cache(context.cacheDir, cacheSize.toLong())) - - if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { - val address = InetSocketAddress.createUnresolved(httpServer, httpPort) - builder.proxy(Proxy(Proxy.Type.HTTP, address)) - } - - // trust the new Let's Encrypt root certificate that is not available on Android < 7.1.1 - // new cert https://letsencrypt.org/certs/isrgrootx1.pem - // see https://letsencrypt.org/2020/11/06/own-two-feet.html - // see https://stackoverflow.com/questions/64844311/certpathvalidatorexception-connecting-to-a-lets-encrypt-host-on-android-m-or-ea - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - val isgCert = """ - -----BEGIN CERTIFICATE----- - MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw - TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh - cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 - WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu - ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY - MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc - h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ - 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U - A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW - T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH - B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC - B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv - KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn - OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn - jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw - qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI - rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV - HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq - hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL - ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ - 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK - NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 - ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur - TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC - jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc - oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq - 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA - mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d - emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= - -----END CERTIFICATE----- - """.trimIndent() - val cf = CertificateFactory.getInstance("X.509") - val isgCertificate = cf.generateCertificate(ByteArrayInputStream(isgCert.toByteArray(charset("UTF-8")))) - val certificates = HandshakeCertificates.Builder() - .addTrustedCertificate(isgCertificate as X509Certificate) - .addPlatformTrustedCertificates() - .build() - builder.sslSocketFactory( - certificates.sslSocketFactory(), - certificates.trustManager - ) - } - return builder -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt index 2ad4b825d..c78b0f787 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt @@ -1,5 +1,6 @@ package com.keylesspalace.tusky.util +import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable @@ -9,6 +10,7 @@ open class RxAwareViewModel : ViewModel() { fun Disposable.autoDispose() = disposables.add(this) + @CallSuper override fun onCleared() { super.onCleared() disposables.clear() diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java index 690098309..29693550d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java @@ -1,33 +1,18 @@ package com.keylesspalace.tusky.util; -import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; import android.net.Uri; -import android.os.AsyncTask; -import android.text.TextUtils; import android.util.Log; -import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; -import com.keylesspalace.tusky.entity.NewPoll; -import com.keylesspalace.tusky.entity.Status; -import java.io.File; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; import javax.inject.Inject; @@ -45,61 +30,6 @@ public final class SaveTootHelper { this.context = context; } - @SuppressLint("StaticFieldLeak") - public boolean saveToot(@NonNull String content, - @NonNull String contentWarning, - @Nullable List savedJsonUrls, - @NonNull List mediaUris, - @NonNull List mediaDescriptions, - int savedTootUid, - @Nullable String inReplyToId, - @Nullable String replyingStatusContent, - @Nullable String replyingStatusAuthorUsername, - @NonNull Status.Visibility statusVisibility, - @Nullable NewPoll poll) { - - if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) { - return false; - } - - // Get any existing file's URIs. - - String mediaUrlsSerialized = null; - String mediaDescriptionsSerialized = null; - - if (!ListUtils.isEmpty(mediaUris)) { - List savedList = saveMedia(mediaUris, savedJsonUrls); - if (!ListUtils.isEmpty(savedList)) { - mediaUrlsSerialized = gson.toJson(savedList); - if (!ListUtils.isEmpty(savedJsonUrls)) { - deleteMedia(setDifference(savedJsonUrls, savedList)); - } - } else { - return false; - } - mediaDescriptionsSerialized = gson.toJson(mediaDescriptions); - } else if (!ListUtils.isEmpty(savedJsonUrls)) { - /* If there were URIs in the previous draft, but they've now been removed, those files - * can be deleted. */ - deleteMedia(savedJsonUrls); - } - final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning, - inReplyToId, - replyingStatusContent, - replyingStatusAuthorUsername, - statusVisibility, - poll); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - tootDao.insertOrReplace(toot); - return null; - } - }.execute(); - return true; - } - public void deleteDraft(int tootId) { TootEntity item = tootDao.find(tootId); if (item != null) { @@ -124,82 +54,4 @@ public final class SaveTootHelper { tootDao.delete(item.getUid()); } - @Nullable - private List saveMedia(@NonNull List mediaUris, - @Nullable List existingUris) { - - File directory = context.getExternalFilesDir("Tusky"); - - if (directory == null || !(directory.exists())) { - Log.e(TAG, "Error obtaining directory to save media."); - return null; - } - - ContentResolver contentResolver = context.getContentResolver(); - ArrayList filesSoFar = new ArrayList<>(); - ArrayList results = new ArrayList<>(); - for (String mediaUri : mediaUris) { - /* If the media was already saved in a previous draft, there's no need to save another - * copy, just add the existing URI to the results. */ - if (existingUris != null) { - int index = existingUris.indexOf(mediaUri); - if (index != -1) { - results.add(mediaUri); - continue; - } - } - // Otherwise, save the media. - - Uri uri = Uri.parse(mediaUri); - - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); - - String mimeType = contentResolver.getType(uri); - MimeTypeMap map = MimeTypeMap.getSingleton(); - String fileExtension = map.getExtensionFromMimeType(mimeType); - String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension); - File file = new File(directory, filename); - filesSoFar.add(file); - boolean copied = IOUtils.copyToFile(contentResolver, uri, file); - if (!copied) { - /* If any media files were created in prior iterations, delete those before - * returning. */ - for (File earlierFile : filesSoFar) { - boolean deleted = earlierFile.delete(); - if (!deleted) { - Log.i(TAG, "Could not delete the file " + earlierFile.toString()); - } - } - return null; - } - Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file); - results.add(resultUri.toString()); - } - return results; - } - - private void deleteMedia(List mediaUris) { - for (String uriString : mediaUris) { - Uri uri = Uri.parse(uriString); - if (context.getContentResolver().delete(uri, null, null) == 0) { - Log.e(TAG, String.format("Did not delete file %s.", uriString)); - } - } - } - - /** - * A∖B={x∈A|x∉B} - * - * @return all elements of set A that are not in set B. - */ - private static List setDifference(List a, List b) { - List c = new ArrayList<>(); - for (String s : a) { - if (!b.contains(s)) { - c.add(s); - } - } - return c; - } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt index 484669965..307fbeae7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -18,7 +18,7 @@ private const val TAG_REGEX = "(?:^|[^/)A-Za-z0-9_])#([\\w_]*[\\p{Alpha}_][\\w_] * @see * Account#MENTION_RE */ -private const val MENTION_REGEX = "(?:^|[^/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)" +private const val MENTION_REGEX = "(?:^|[^/[:word:]])@([a-z0-9_-]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)" private const val HTTP_URL_REGEX = "(?:(^|\\b)http://[^\\s]+)" private const val HTTPS_URL_REGEX = "(?:(^|\\b)https://[^\\s]+)" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt index b6bd48c0c..bfec20e2e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt @@ -17,6 +17,8 @@ data class StatusDisplayOptions( val confirmReblogs: Boolean, @get:JvmName("hideStats") val hideStats: Boolean, + @get:JvmName("animateEmojis") + val animateEmojis: Boolean, @get:JvmName("quoteEnabled") val quoteEnabled: Boolean ) 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 2fb9ad428..822100290 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -228,7 +228,8 @@ class StatusViewHelper(private val itemView: View) { 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) - else -> context.getString(R.string.status_media_images) + Attachment.Type.AUDIO -> context.getString(R.string.status_media_audio) + else -> context.getString(R.string.status_media_attachments) } } @@ -237,11 +238,12 @@ class StatusViewHelper(private val itemView: View) { return when (type) { Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp - else -> R.drawable.ic_photo_24dp + Attachment.Type.AUDIO -> R.drawable.ic_music_box_24dp + else -> R.drawable.ic_attach_file_24dp } } - fun setupPollReadonly(poll: PollViewData?, emojis: List, useAbsoluteTime: Boolean) { + fun setupPollReadonly(poll: PollViewData?, emojis: List, statusDisplayOptions: StatusDisplayOptions) { val pollResults = listOf( itemView.findViewById(R.id.status_poll_option_result_0), itemView.findViewById(R.id.status_poll_option_result_1), @@ -259,10 +261,10 @@ class StatusViewHelper(private val itemView: View) { val timestamp = System.currentTimeMillis() - setupPollResult(poll, emojis, pollResults) + setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis) pollDescription.visibility = View.VISIBLE - pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime) + pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime) } } @@ -290,7 +292,7 @@ class StatusViewHelper(private val itemView: View) { } - private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List) { + private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List, animateEmojis: Boolean) { val options = poll.options for (i in 0 until Status.MAX_POLL_OPTIONS) { @@ -298,7 +300,7 @@ class StatusViewHelper(private val itemView: View) { val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount) val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context) - pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i]) + pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i], animateEmojis) pollResults[i].visibility = View.VISIBLE val level = percent * 100 diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 1b254c715..6b75ad815 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -52,7 +52,7 @@ public final class ViewDataUtils { .setSensitive(visibleStatus.getSensitive()) .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) .setSpoilerText(visibleStatus.getSpoilerText()) - .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getUsername()) + .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) .setUserFullName(visibleStatus.getAccount().getName()) .setVisibility(visibleStatus.getVisibility()) .setSenderId(visibleStatus.getAccount().getId()) @@ -60,6 +60,7 @@ public final class ViewDataUtils { .setApplication(visibleStatus.getApplication()) .setStatusEmojis(visibleStatus.getEmojis()) .setAccountEmojis(visibleStatus.getAccount().getEmojis()) + .setRebloggedByEmojis(status.getReblog() == null ? null : status.getAccount().getEmojis()) .setCollapsible(SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) .setCollapsed(true) .setPoll(visibleStatus.getPoll()) diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index 44a70267e..435e24501 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -4,6 +4,7 @@ package com.keylesspalace.tusky.view import android.app.Activity import android.widget.CheckBox +import android.widget.Spinner import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R @@ -11,7 +12,7 @@ import com.keylesspalace.tusky.R fun showMuteAccountDialog( activity: Activity, accountUsername: String, - onOk: (notifications: Boolean) -> Unit + onOk: (notifications: Boolean, duration: Int) -> Unit ) { val view = activity.layoutInflater.inflate(R.layout.dialog_mute_account, null) (view.findViewById(R.id.warning) as TextView).text = @@ -21,7 +22,11 @@ fun showMuteAccountDialog( AlertDialog.Builder(activity) .setView(view) - .setPositiveButton(android.R.string.ok) { _, _ -> onOk(checkbox.isChecked) } + .setPositiveButton(android.R.string.ok) { _, _ -> + val spinner: Spinner = view.findViewById(R.id.duration) + val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) + onOk(checkbox.isChecked, durationValues[spinner.selectedItemPosition]) + } .setNegativeButton(android.R.string.cancel, null) .show() } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index 6b89e9e25..c5f1050bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -86,6 +86,7 @@ public abstract class StatusViewData { private final Status.Application application; private final List statusEmojis; private final List accountEmojis; + private final List rebloggedByAccountEmojis; @Nullable private final Card card; private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */ @@ -103,7 +104,7 @@ public abstract class StatusViewData { boolean isShowingContent, String userFullName, String nickname, String avatar, Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, - Status.Application application, List statusEmojis, List accountEmojis, @Nullable Card card, + Status.Application application, List statusEmojis, List accountEmojis, List rebloggedByAccountEmojis, @Nullable Card card, boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, Status quote, boolean isNotestock) { this.id = id; @@ -140,6 +141,7 @@ public abstract class StatusViewData { this.application = application; this.statusEmojis = statusEmojis; this.accountEmojis = accountEmojis; + this.rebloggedByAccountEmojis = rebloggedByAccountEmojis; this.card = card; this.isCollapsible = isCollapsible; this.isCollapsed = isCollapsed; @@ -267,6 +269,10 @@ public abstract class StatusViewData { return accountEmojis; } + public List getRebloggedByAccountEmojis() { + return rebloggedByAccountEmojis; + } + @Nullable public Card getCard() { return card; @@ -341,6 +347,7 @@ public abstract class StatusViewData { Objects.equals(application, concrete.application) && Objects.equals(statusEmojis, concrete.statusEmojis) && Objects.equals(accountEmojis, concrete.accountEmojis) && + Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) && Objects.equals(card, concrete.card) && Objects.equals(poll, concrete.poll) && isCollapsed == concrete.isCollapsed && @@ -447,6 +454,7 @@ public abstract class StatusViewData { private Status.Application application; private List statusEmojis; private List accountEmojis; + private List rebloggedByAccountEmojis; private Card card; private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */ private boolean isCollapsed; /** Whether the status is shown partially or fully */ @@ -639,6 +647,11 @@ public abstract class StatusViewData { return this; } + public Builder setRebloggedByEmojis(List emojis) { + this.rebloggedByAccountEmojis = emojis; + return this; + } + public Builder setCard(Card card) { this.card = card; return this; @@ -692,7 +705,7 @@ public abstract class StatusViewData { visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, - statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, quote, isNotestock); + statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot, quote, isNotestock); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index b2568f34e..a0f0ed680 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -119,8 +119,8 @@ class AccountViewModel @Inject constructor( } } - fun muteAccount(notifications: Boolean) { - changeRelationship(RelationShipAction.MUTE, notifications) + fun muteAccount(notifications: Boolean, duration: Int) { + changeRelationship(RelationShipAction.MUTE, notifications, duration) } fun unmuteAccount() { @@ -187,7 +187,7 @@ class AccountViewModel @Inject constructor( /** * @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE */ - private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null) { + private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null, duration: Int? = null) { val relation = relationshipData.value?.data val account = accountData.value?.data val isMastodon = relationshipData.value?.data?.notifying != null @@ -227,7 +227,7 @@ class AccountViewModel @Inject constructor( RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) - RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) + RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) RelationShipAction.SUBSCRIBE -> { if(isMastodon) diff --git a/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java b/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java index 207e3b341..698214ff5 100644 --- a/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java +++ b/app/src/main/java/net/accelf/yuito/QuoteInlineHelper.java @@ -55,7 +55,7 @@ public class QuoteInlineHelper { } private void setDisplayName(String name, List customEmojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, quoteDisplayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, quoteDisplayName, statusDisplayOptions.animateEmojis()); quoteDisplayName.setText(emojifiedName); } @@ -69,7 +69,7 @@ public class QuoteInlineHelper { private void setContent(Spanned content, Status.Mention[] mentions, List emojis, LinkListener listener) { Spanned singleLineText = SpannedTextHelper.replaceSpanned(content); - CharSequence emojifiedText = CustomEmojiHelper.emojify(singleLineText, emojis, quoteContent); + CharSequence emojifiedText = CustomEmojiHelper.emojify(singleLineText, emojis, quoteContent, statusDisplayOptions.animateEmojis()); LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, listener); } @@ -79,7 +79,7 @@ public class QuoteInlineHelper { private void setSpoilerText(String spoilerText, List emojis) { CharSequence emojiSpoiler = - CustomEmojiHelper.emojify(spoilerText, emojis, quoteContentWarningDescription); + CustomEmojiHelper.emojify(spoilerText, emojis, quoteContentWarningDescription, statusDisplayOptions.animateEmojis()); quoteContentWarningDescription.setText(emojiSpoiler); quoteContentWarningDescription.setVisibility(View.VISIBLE); quoteContentWarningButton.setVisibility(View.VISIBLE); diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 000000000..4c894f0dc --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notebook.xml b/app/src/main/res/drawable/ic_notebook.xml index 2395cd141..93ff78919 100644 --- a/app/src/main/res/drawable/ic_notebook.xml +++ b/app/src/main/res/drawable/ic_notebook.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml index 52de2b95a..150f0860c 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -1,8 +1,10 @@ + android:background="?attr/windowBackgroundColor" + tools:viewBindingIgnore="true"> + + diff --git a/app/src/main/res/layout/activity_drafts.xml b/app/src/main/res/layout/activity_drafts.xml new file mode 100644 index 000000000..3de886cb7 --- /dev/null +++ b/app/src/main/res/layout/activity_drafts.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_mute_account.xml b/app/src/main/res/layout/dialog_mute_account.xml index 673fc9e44..b58a277cb 100644 --- a/app/src/main/res/layout/dialog_mute_account.xml +++ b/app/src/main/res/layout/dialog_mute_account.xml @@ -22,4 +22,17 @@ app:buttonTint="@color/compound_button_color" android:text="@string/dialog_mute_hide_notifications"/> - \ No newline at end of file + + + + + diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml index 433f1ed0f..806d420a4 100644 --- a/app/src/main/res/layout/fragment_view_thread.xml +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -1,9 +1,11 @@ + android:layout_gravity="top" + tools:viewBindingIgnore="true"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/item_draft.xml b/app/src/main/res/layout/item_draft.xml new file mode 100644 index 000000000..5708e08f1 --- /dev/null +++ b/app/src/main/res/layout/item_draft.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_media_preview.xml b/app/src/main/res/layout/item_media_preview.xml new file mode 100644 index 000000000..27b58e7a1 --- /dev/null +++ b/app/src/main/res/layout/item_media_preview.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 053be82d5..b421aa873 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -270,192 +270,8 @@ app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintTop_toBottomOf="@id/status_quote_inline_container" tools:visibility="visible"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index d176456be..245de1578 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -218,189 +218,7 @@ android:importantForAccessibility="noHideDescendants" app:layout_constraintTop_toBottomOf="@id/status_card_view"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -613,4 +431,4 @@ app:layout_constraintTop_toTopOf="@id/status_reply" app:srcCompat="@drawable/ic_more_horiz_24dp" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/toolbar_basic.xml b/app/src/main/res/layout/toolbar_basic.xml index 71039105f..47bd2d90f 100644 --- a/app/src/main/res/layout/toolbar_basic.xml +++ b/app/src/main/res/layout/toolbar_basic.xml @@ -1,19 +1,16 @@ - + - + android:layout_height="?attr/actionBarSize" /> - - - - - \ 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 ee8811228..d25bcdc17 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -2,18 +2,10 @@ - - - - diff --git a/app/src/main/res/menu/drafts.xml b/app/src/main/res/menu/drafts.xml new file mode 100644 index 000000000..bbc9202f4 --- /dev/null +++ b/app/src/main/res/menu/drafts.xml @@ -0,0 +1,10 @@ + + + + \ 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 364ad1770..0b9833922 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -36,7 +36,7 @@ الحسابات المحظورة طلبات المتابعة عدل ملفك التعريفي - المسودات + المسودات الرّخص \@%s شارَكَه %s @@ -441,13 +441,13 @@ إضافة استطلاع رأي افتح دائما التبويقات التي تحتوي على محتوى حساس استطلاع رأي - 5 دقائق - 30 دقيقة - ساعة واحدة - 6 ساعات - يوم واحد - 3 أيام - 7 أيام + 5 دقائق + 30 دقيقة + ساعة واحدة + 6 ساعات + يوم واحد + 3 أيام + 7 أيام ضف خيارا خيارات متعددة الخيار %d diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index 2e0412ca4..9726d4ff0 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -16,7 +16,7 @@ ⵏⴰⴸⵉ ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ ⴼⴼⴻⵖ - ⵉⵔⴻⵡⵡⴰⵢⴻⵏ + ⵉⵔⴻⵡⵡⴰⵢⴻⵏ ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ ⵓⵖⴰⵍ ⴽⴻⵎⵎⴻⵍ diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..61c97505f --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,506 @@ + + + Публикация + Публикацията, на която сте изготвили отговор, е премахната + 1 час + 30 минути + 5 минути + Неопределено + Продължителност + Анкета + Активиране на плъзгащия жест за превключване между раздели + Показване на филтър за известия + Търсенето бе неуспешно + Акаунти + Акаунтът е от друг сървър. Да изпратите ли и там анонимно копие на доклада\? + Докладът ще бъде изпратен на модератора на вашия сървър. Можете да предоставите обяснение защо докладвате този акаунт по-долу: + Извличането на състояния бе неуспешно + Докладването бе неуспешно + Препращане към %s + Допълнителни коментари + Успешно докладване на @%s + Готово + Назад + Продължаване + + Остава %d секунда + Остават %d секунди + + + Остава %d минута + Остават %d минути + + + Остава %d час + Остават %d часа + + + Остава %d ден + Остават %d дни + + Анкета, която създадохте, приключи + Анкета, в която сте гласували, приключи + Гласуване + затворено + завършва в %s + + %s човек + %s човека + + + %s глас + %s гласа + + " <!-- 15 votes • 1 hour left --> %1$s • %2$s" + %1$s • %2$s + Действия за изображение %s + Сигурни ли сте, че искате да изчистите окончателно всичките си известия\? + Композиране + Композиране на публикация + Прилагане + Филтриране + Изчистване + Списък + Избиране на списък + Хаштагове + Хаштаг без # + Добавяне на хаштаг + Име на списък + Анкета с избори: %1$s, %2$s, %3$s, %4$s; %5$s + Директно + Последователи + Публично + Отметнато + Поставено в любими + Реблог + Без описание + Предупреждение за съдържание: %s + Мултимедия: %s + достигнати са максималните %1$d раздела + %1$s, %2$s и %3$d други + %1$s + %1$s и %2$s + Поставено в любими от + Споделено от + + <b>%s</b> Споделяне + <b>%s</b> Споделяния + + + <b>%1$s</b> Любимо + <b>%1$s</b> Любими + + Закачане + Разкачане + Информацията по-долу може да отразява непълно потребителския профил. Натиснете, за да отворите пълен профил в браузъра. + Използване на абсолютно време + Съдържание + Етикет + добавяне на данни + Профилни метаданни + CC-BY-SA 4.0 + CC-BY 4.0 + Лицензиран под лиценза Apache (копие по-долу) + Tusky съдържа код и активи от следните проекти с отворен код: + Отсподеляне + Споделяне с оригиналната аудитория + %1$s се премести в: + Бот + Изтеглянето се провали + Текущият набор от емоджита на Google + Първо ще трябва да изтеглите тези емоджи комплекти + Стандартният емоджи комплект на Mastodon + Blob емоджитата, известни от Android 4.4–7.1 + Емоджи комплектът по подразбиране в устройство ви + Рестартиране + По-късно + Ще трябва да рестартирате Tusky, за да приложите тези промени + Изисква се рестартиране на приложението + Отваряне на публикация + Разгъване/свиване на всички състояния + Извършва се търсене… + По подразбиране от системата + Стил на емоджи + Копирано в клипборда + Инстанцията ви %s няма персонализирани емоджита + Композиране + Копие от публикацията е запазено във вашите чернови + Изпращането е отменено + Изпращане на публикации + Грешка при изпращане на публикация + Изпращане на публикация… + Запазване на чернова\? + Изисква ръчно одобряване на последователи + Заключване на акаунт + Премахване + Задаване на надпис + Опишете за хора със зрителни увреждания +\n(%d ограничение на знаците) + Неуспешно задаване на надпис + Публикуване с акаунт %1$s + Премахване на акаунт от списъка + Добавяне на акаунт към списъка + Търсене на хора, които следвате + Редакция на списъка + Изтриване на списъка + Преименуване на списъка + Създаване на списък + Списъкът не можа да се изтрие + Списъкът не можа да се създаде + Списъкът не можа да се преименува + Списъчна емисия + Списъци + Списъци + Добавяне на нов Mastodon акаунт + Добавяне на акаунт + Фраза за филтриране + Когато ключовата дума или фраза е само буквено-цифрова, тя ще бъде приложена само ако съответства на цялата дума + Цяла дума + Актуализиране + Премахване + Редакция на филтър + Добавяне на филтър + Разговори + Публични емисии + зареждане на още + Отговаряне на @%s + Мултимедия + Винаги разгъване на публикации, маркирани с предупреждения за съдържание + Винаги показване на деликатно съдържание + Следва ви + %dс + %dм + %dч + %dд + %dг + след %dс + след %dм + след %dч + след %dг + след %dд + Заявено последване + Прикачени файлове + Аудио + Видео + Изображения + Споделяне на връзка към публикация + Споделяне на съдържание на публикация + Профилът на Tusky + Доклади за грешки и заявки за функции: +\n https://github.com/tuskyapp/Tusky/issues + Уебсайт на проекта: +\n https://tusky.app + Tusky е свободен софтуер с отворен код. Той е лицензиран под Общият публичен лиценз на GNU Версия 3. Можете да видите лиценза тук: https://www.gnu.org/licenses/gpl-3.0.en.html + Осъществено от Tusky + Tusky %s + Относно + Заключен акаунт + %d нови взаимодействия + %1$s и %2$s + %1$s, %2$s, и %3$s + %1$s, %2$s, %3$s и %4$d други + %s ви спомена + Известия, когато някой, за когото сте абонирани, публикува + Нови публикации + Известия за приключили анкети + Анкети + Известия, когато публикациите ви бъдат означени като любими + Любими + Известия, когато публикациите ви се споделят + Най-малък + Скрито + Раздели + Филтриране на емисия + Анимиране на персонализирани емоджита + Показване на цветни градиенти за скрита мултимедия + Анимиране на GIF аватари + Показване на индикатор за ботове + Език + Скриване на бутона за композиране, при превъртане + Използване на персонализирани раздели чрез Chrome + Браузър + Използване на системния дизайн + Автоматично при залез + Черно + Светло + Тъмно + Филтри + Емисии + Тема на приложение + Външен вид + някой, за когото съм абониран, публикува + приключили анкети + публикациите ми са сложени в любими + публикациите ми са споделени + заявка за последване + последвани + споменати + Уведомете ме когато + Уведомяване със светлина + Уведомяване с вибрация + Уведомяване със звук + Сигнали + Известия + Известия + Директно: Публикуване само за споменатите потребители + Само за последователи: Публикуване само за последователи + Публично: Публикуване в публични емисии + Скриване на известия + Заглушаване на @%s\? + Блокиране на @%s\? + Скриване на целия домейн + Сигурни ли сте, че искате да блокирате всички от %s\? Няма да виждате съдържание от този домейн в нито една публична емисия или във вашите известия. Последователите ви от този домейн ще бъдат премахнати. + Изтриване и преработване на тази публикация\? + Изтриване на тази публикация\? + Отследване на този акаунт\? + Отмяна на заявката за последване\? + Изтегляне + Качване… + Завършване на мултимедийно качване + "Тук може да се въведе адресът или домейнът на която и да е инстанция, като 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. + + Свързване… + Какво е инстанция\? + Заглавна част + Аватар + Отговор… + Няма резултати + Търсене… + Био + Показвано име + Предупреждение за съдържание + Какво се случва\? + Коя инстанция\? + Отговорът е изпратен успешно. + Изпратено! + %s е разкрит + Потребителят е раззаглушен + Потребителят е деблокиран + Изпратено! + Споделяне на мултимедия в… + Споделяне на публикация в… + Споделяне на URL адреса на публикацията в… + Теглене на мултимедия + Изтегляне на мултимедия + Споделяне като … + Отваряне като %s + Копиране на връзката + Изтегляне на %1$s + Отваряне на мултимедия #%d + Връзки + Споменавания + Хаштагове + Показване на любими + Показване на споделяния + Отваряне на споделилия автор + Хаштагове + Споменавания + Връзки + Добавяне на раздел + Нулиране + Планиране на публикация + Емоджи клавиатура + Предупреждение за съдържание + Видимост на публикация + Планирани публикации + Чернови + Търсене + Отхвърляне + Приемане + Отмяна + Редакция + Редакция на профил + Запазване + Отваряне на чекмедже + Скриване на мултимедия + Споменаване + Раззаглушаване на разговор + Заглушаване на разговор + Раззаглушаване на %s + Заглушаване на %s + Заглушаване на известия от %s + Раззаглушаване на известия от %s + Раззаглушаване на %s + Раззаглушаване + Заглушаване + Споделяне + Снимане + Добавяне на анкета + Добавяне на мултимедия + Отваряне в браузър + Мултимедия + Заявки за последване + Скрити домейни + Блокирани потребители + Заглушени потребители + Отметки + Любими + Предпочитания за акаунт + Предпочитания + Профил + Затваряне + Повторен опит + ПУБЛИКУВАНЕ! + ИЗПРАЩАНЕ + Изтриване и преработване + Изтриване + Редакция + Докладване + Показване на споделяния + Скриване на споделяния + Деблокиране + Блокиране + Отследване + Последване + Сигурни ли сте, че искате да излезете от акаунта %1$s\? + Излизане + Влизане с Mastodon + Композиране + Още + Премахване от любими + Отмятане + Поставяне в любими + Премахване на споделяне + Споделяне + Отговор + Бърз отговор + Допълнителни коментари\? + Докладване на @%s + %s току-що публикува + %s поиска да ви последва + %s ви последва + %s постави вашата публикация в любими + %s сподели вашата публикация + Нищо тук. Дръпнете надолу, за да опресните! + Нищо тук. + Свиване + Разгъване + Покажи по-малко + Покажи повече + Щракнете за преглед + Мултимедията е скрита + Деликатно съдържание + %s сподели + \@%s + Лицензи + Оповестявания + Планирани публикации + Чернови + Редакция на профила ви + Заявки за последване + Скрити домейни + Блокирани потребители + Заглушени потребители + Отметки + Любими + Последователи + Последвани + Закачени + С отговори + Публикации + Раздели + Директни съобщения + Локално + Известия + Начало + Грешка при изпращане на публикация. + Качването бе неуспешно. + Изображения и видеоклипове не могат да бъдат прикачени към едно и също състояние. + Изисква се разрешение за съхранение на мултимедия. + Изисква се разрешение за четене на носител. + Този файл не можа да бъде отворен. + Този тип файл не може да бъде качен. + Аудио файловете трябва да са по-малки от 40MB. + Видео файловете трябва да са по-малки от 40MB. + Файлът трябва да е по-малък от 8MB. + Състоянието е твърде дълго! + Получаването на токен за вход бе неуспешно. + Упълномощаването е отказано. + Възникна неидентифицирана грешка при упълномощаване. + Неуспешно намиране на уеб браузър, който да се използва. + Неуспешно удостоверяване с тази инстанция. + Въведен е невалиден домейн + Това не може да бъде празно. + Възникна грешка в мрежата! Моля, проверете връзката си и опитайте отново! + Възникна грешка. + Черновата е изтрита + Неуспешно зареждане на информация за отговор + Стари чернови + Функцията за чернови в Tusky е напълно преработена, за да бъде по-бърза, по-лесна за ползване и по-малко бъгава. +\n Все още можете да осъществите достъп до старите си чернови чрез бутон на екрана за нови чернови, но те ще бъдат премахнати при бъдеща актуализация! + Тази публикация не успя да се изпрати! + Наистина ли искате да изтриете списъка %s\? + Не можете да качите повече от %1$d мултимедийни прикачени файлове. + Скриване на количествена статистика на профили + Скриване на количествена статистика на публикации + Ограничаване на известия от емисия + Преглед на известията + Част от информацията, която може да повлияе на вашето психично състояние, ще бъде скрита. Това включва: +\n +\n - Известия за Любими/Споделяния/Последвани +\n - Брой Любими/Споделяния на публикации +\n - Статистика за Последователи/Публикации на профили +\n +\n Изскачащите известия няма да бъдат засегнати, но можете да прегледате предпочитанията си за известяване ръчно. + Запазено! + Вашата лична бележка за този акаунт + Благосъстояние + Скриване на заглавието на горната лента с инструменти + Показване на диалоговия прозорец за потвърждение преди споделяне + Показване на визуализации на връзки в емисии + Mastodon има минимален интервал за планиране от 5 минути. + Няма оповестявания. + Нямате планирани състояния. + Нямате чернови. + Грешка при търсенето на публикация %s + Редакция + Избор %d + Множество избора + Добавяне на избор + 7 дни + 3 дни + 1 ден + 6 часа + Споделяния + Известия за заявки за последване + Заявки за последване + Известия за нови последователи + Нови последователи + Известия за нови споменавания + Нови споменавания + Най-голям + Голям + Среден + Малък + Скрито: Не се показва в публични емисии + Размер на текста на състоянието + Само за последователи + Скрито + Публично + Долу + Горе + Основна навигационна позиция + Синхронизирането на настройките бе неуспешно + Публикуване (синхронизирано със сървър) + Винаги маркиране на мултимедия като чувствителна + Поверителност на публикация по подразбиране + HTTP прокси порт + HTTP прокси сървър + Активиране на HTTP прокси + HTTP прокси + Прокси + Изтегляне на визуализации за мултимедии + Показване на отговори + Показване на споделяния + \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 2fb78dd8f..72fb77e40 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -292,7 +292,7 @@ মিডিয়া লুকানো সংবেদনশীল কন্টেন্ট লাইসেন্সগুলি - খসড়াগুলো + খসড়াগুলো আপনার প্রোফাইল সম্পাদনা করুন অনুরোধ অনুসরণ করুন অবরুদ্ধ ব্যবহারকারী @@ -345,19 +345,19 @@ পছন্দ %d একাধিক পছন্দ পছন্দ যুক্ত করুন - ৭ দিন - ৩ দিন - ১ দিন - ৬ ঘন্টা - ১ ঘন্টা - ৩০ মিনিট - ৫ মিনিট + ৭ দিন + ৩ দিন + ১ দিন + ৬ ঘন্টা + ১ ঘন্টা + ৩০ মিনিট + ৫ মিনিট ভোটগ্রহণ সরান পোল যুক্ত করুন সর্বদা সামগ্রী সতর্কতা সহ চিহ্নিত টুটগুলি প্রসারিত করুন অনুসন্ধান করতে ব্যর্থ - অক্কোউন্টগুলি + অ্যাকাউন্টগুলো যখন শব্দ বা বাক্যাংশটি শুধুমাত্র আলফানিউমেরিক হয় তখন এটি শুধুমাত্র তখনই প্রয়োগ করা হবে যদি এটি সম্পূর্ণ শব্দটির সাথে মেলে সম্পূর্ণ শব্দ বিজ্ঞপ্তি ফিল্টার দেখান @@ -424,4 +424,15 @@ এই জায়গা খালি হতে পারে না। একটি নেটওয়ার্ক ত্রুটি ঘটেছে! আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন! একটি ত্রুটি ঘটেছে। + + %1$sটি পছন্দ + %1$sটি পছন্দ + + %s দৃশ্যমান + %s পোস্ট করেছে + %s তোমাকে ফলো করতে চায় + %s তোমাকে ফলো করেছে + %s তোমার টুট বুস্ট করেছে + %s তোমার টুট বুস্ট করেছে + ঘোষণা \ No newline at end of file diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 0a07a433d..fac3ef580 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -36,7 +36,7 @@ অবরুদ্ধ ব্যবহারকারী অনুরোধ অনুসরণ করুন আপনার প্রোফাইল সম্পাদনা করুন - খসড়াগুলো + খসড়াগুলো লাইসেন্সগুলি \@%s %s সমর্থন দিয়েছে @@ -395,13 +395,13 @@ অনুসন্ধান করতে ব্যর্থ পোল যুক্ত করুন ভোটগ্রহণ - ৫ মিনিট - ৩০ মিনিট - ১ ঘন্টা - ৬ ঘন্টা - ১ দিন - ৩ দিন - ৭ দিন + ৫ মিনিট + ৩০ মিনিট + ১ ঘন্টা + ৬ ঘন্টা + ১ দিন + ৩ দিন + ৭ দিন পছন্দ যুক্ত করুন একাধিক পছন্দ পছন্দ %d diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c6a9eecfb..9aa5d8804 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -2,20 +2,20 @@ S\'ha produït un error. Això no pot estar buit. - El domini introduït no és vàlid - L\'autenticació en aquesta instància ha fallat. + El domini que s\'ha introduït no és vàlid + Ha fallat l\'autenticació en aquesta instància. No s\'ha trobat cap navegador web per a utilitzar. S\'ha produït un error d\'autorització no identificat. S\'ha denegat l\'autorització. - L\'obtenció del token d\'inici de sessió ha fallat. + Ha fallat l\'obtenció del token d\'inici de sessió. L\'estat és massa llarg! - El fitxer ha de ser inferior a 8MB. - Aquest tipus de fitxer no es pot pujar. - Aquest tipus de fitxer no es pot obrir. - Cal permís d\'accés al emmagatzematge. - Cal permís d\'escriptura en el dispositiu. + El fitxer ha de ser d\'una mida menor de 8MB. + No es pot pujar aquest tipus de fitxer. + No es pot obrir aquest tipus de fitxer. + Cal permís d\'accés a l\'emmagatzematge. + Cal permís d\'escriptura a l\'emmagatzematge. No es poden adjuntar imatges i vídeos en el mateix estat. - La pujada ha fallat. + Ha fallat la pujada. Inici Notificacions Local @@ -29,7 +29,7 @@ Usuaris blocats Peticions de seguiment Edita el perfil - Esborranys + Esborranys \@%s %s tootejat Contingut sensible @@ -107,7 +107,7 @@ , però pots comunicar-te fàcilment i seguir amics d\'altres instàncies com si fossiu en el mateix lloc. \n\nTens més informació a joinmastodon.org. - S\'està finalitzant la pujada de materila multimèdia + S\'està finalitzant la pujada de material multimèdia S\'està pujant… Baixa Vols deixar de seguir aquest compte? @@ -191,7 +191,7 @@ En resposta a @%s carrega\'n més Vota - S\'ha produït un error en enviar el toot. + S\'ha produït un error en enviar el tut. Pestanyes Llicències Amplia @@ -202,15 +202,15 @@ Missatges directes No hi ha res aquí. Elimina l\'impuls - S\'ha produït un error de connexió! Comprova la connexió i torna-ho a provar! - Els fitxers de vídeo han de pesar menys de 40 MB. + 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 Estàs segur de tancar la sessió de %1$s\? Amaga els retoots Mostra els impulsos Elimina i reecririu - Open drawer + Obre el menú Visibilitat del toot Contingut sensible Afegir una pestanya @@ -225,7 +225,7 @@ Baixa el fitxer Compartir la imatge a … Enviat! - Follow requested + S\'ha enviat la petició de seguiment Amb respostes Teclat d\'emojis Obrir el media #%d @@ -334,8 +334,8 @@ %1$s Favorits - - + %s impuls + %s impulsos Impulsat per Marcat favorit per @@ -361,7 +361,7 @@ Vols netejar totes les notificacions permanentment\? %1$s • %2$s - %s vots + %s vot %s vots Acaba a %s @@ -399,16 +399,16 @@ \@%s reportat satisfactoriament El compte és d\'un altre servidor. Enviar, igualment, una copia anònima del report\? Cerca fallida - 1 hora - 6 hores + 1 hora + 6 hores Edita Afegeix una enquesta Enquesta - 5 minuts - 30 minuts - 1 dia - 3 dies - 7 dies + 5 minuts + 30 minuts + 1 dia + 3 dies + 7 dies Afegeix una tria Múltiples tries Tria %d @@ -421,13 +421,13 @@ Programar el toot Reiniciar Desenvolupat per Tusky - 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. - Els fitxers d\'àudio han de ser més petits que 40MB. - No tens cap esborrany + 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. Peticions de seguiment Mostra el diàleg de confirmació abans de promoure @@ -455,4 +455,46 @@ Desactivar les notificacions per %s Activar les notificacions per %s Deixar de silenciar %s + Revisió d\'avisos + S\'ha desat! + Les vostres notes quant a aquest compte + Benestar + Amaga el títol de la barra d\'eines superior + No hi ha cap avís. + Indefinit + Durada + + falta %d segon + falten %d segons + + + falta %d minut + falten %d minuts + + + falta %d hora + falten %d hores + + + falta %d dia + falten %d dies + + 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 eliminat l\'esborrany + No s\'ha pogut carregar la informació de la resposta + Esborranys antics + 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. + Amaga les estadístiques quantitatives dels perfils + Amaga les estadístiques quantitatives de les publicacions + Limita les notificacions de la cronologia \ No newline at end of file diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml new file mode 100644 index 000000000..56f4f5e95 --- /dev/null +++ b/app/src/main/res/values-ckb/strings.xml @@ -0,0 +1,479 @@ + + + چی خەریکه ڕوودەدات؟ + کام نموونە؟ + وەڵام دانەوە کە بە سەرکەوتوویی نێردرا. + ناردن! + %s نەشاراوە + بەکارهێنەر نەگۆڕاو + بەکارهێنەر بەربەست نەکراوە + ناردن! + هاوبەشکردنی میدیا بۆ… + هاوبەشی کردن بە توت بۆ… + هاوبەشکردنی توتی URL بۆ… + داگرتنی میدیا + داگرتنی میدیا + هاوبەش کردن وەک … + کردنەوە وەک %s + بەستەرەکە ڕوونوس بکە + داگرتنی %1$s + کردنەوەی میدیا #%d + بەستەرەکان + ئاماژەکان + هاشتاگی + پیشاندانی دڵخوازەکان + پیشاندانی بەهێزکردنەکان + کردنەوەی بەهێزکردنی نووسەر + هاشتاگ + ئاماژەکان + بەستەرەکان + زیادکردنی سەرخشت + ڕیسێت کردن + خشتەی توت + تەختەکلیلی ئیمۆجی + ئاگاداری ناوەڕۆک + بینینی توت + توتی خشتەکراو + ڕەشنووسەکان + گەڕان + ڕەتکردنەوە + ڕازیبون + گەڕانەوە + بژارکردن + دەستکاری پرۆفایل بکە + بپارێزە + کردنەوەی وێنەکێش + شاردنەوەی میدیا + ئاماژە + گفتوگۆی لاببە + نابێدەنگ کردن %s + بێدەنگکردن %s + بێدەنگکردنی ئاگانامەکان لە %s + ئاگانامەکانی لاببە لە %s + نابێدەنگ %s + بێدەنگی لابردن + بێدەنگ + هاوبەش کردن + وێنە بگرە + زیادکردنی ڕاپرسی + زیادکردنی میدیا + کردنەوە لە وێبگەڕ + میدیا + بەدواداچونی داواکاریەکان بکە + دۆمەینە شاراوەکان + بەکارهێنەرە بلۆککراوەکان + بەکارهێنەرە گۆڕاوەکان + نیشانەکان + دڵخوازەکان + پەسەندکراوەکانی ئەژمێر + پەسەندەکان + پرۆفایل + دابخە + دووبارە هەوڵ بدە + توت! + توت + سڕینەوە و دووبارە-ڕەشنووس + سڕینەوە + دەستکاری + گوزارشەکان + پیشاندانی بەهێزکردنەکان + شاردنەوەی بەهێزکردنەکان + بەربەست کردن لاببە + بلۆک + بەدوادانەچو + بەدواداکەوتن + ئایا دڵنیایت لەوەی دەتەوێت بچیتەدەرەوە لە هەژماری %1$s؟ + چوونەدەرەوە + چوونەژوورەوە لەگەڵ ماستۆدۆن + دروستکردن + زیاتر + لابردنی دڵخوازەکان + نیشانه + دڵخواز + لابردنی بەهێزکردن + بەهێزکردن + وەڵام + وەڵامدانەوەی خێرا + سەرنجەکانی تر؟ + گوزارشت @%s + %s تەنها بڵاوکرایەوە + %s داواکراوە کە شوێنت بکەوێت + %s بەدواتا کەوت + %s خۆشترین توتەکەت + %s توتەکەتی بەرزکردەوە + هیچ شتێک لێرە نیە ڕاکە خوارەوە بۆ نوێکردنەوە! + هیچ شتێک لێرە نیە. + نوشتانەوە + فراوانکردن + کەمتر نیشان بدە + زیاتر پیشان بدە + کرتە بکە بۆ بینین + میدیا شاراوە + ناوەڕۆکی هەستیار + %s بەرزکرا + \@%s + مۆڵەتەکان + ڕاگه یه نراوەکان + توتی خشتەکراو + دەستکاری پرۆفایلەکەت بکە + بەدواداچونی داواکاریەکان بکە + دۆمەینە شاراوەکان + بەکارهێنەرە بلۆککراوەکان + بەکارهێنەرە بێدەنگ + نیشانەکان + دڵخوازەکان + شوێنکەوتوان + بەدوادا + چەسپا + لەگەڵ وەڵامەکان + بابەتەکان + توت + سەرخشتەکان + نامە ڕاستەوخۆکان + گشتی + ناوخۆیی + ئاگادارییەکان + سەرەتا + هەڵە لە ناردنی توت. + بارکردن سەرکەوتوو نەبوو. + وێنە و ڤیدیۆکان ناتوانرێت هەردووک هاوپێچ بکرێت لەگەڵ یەک دۆخ. + مۆڵەت بۆ پاشکەوتکردنی میدیا پێویستە. + مۆڵەت بۆ خوێندنەوەی میدیا پێویستە. + ئەم فایلە ناتوانرێت بکرێتەوە. + ناتوانرێت ئەو جۆرە فایلە باربکرێت. + فایلەکانی دەنگ دەبێت کەمتر بێت لە ٤٠MB. + پێویستە فایلەکانی ڤیدیۆ کەمتر لە 40 مێگابایت بن. + فایلەکە دەبێت کەمتر بێت لە 8 مێگابایت. + ڕەستە زۆر درێژە! + سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. + ڕێپێدان ڕەتکرایەوە. + هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا. + نەیتوانی وێبگەڕبدۆزێتەوە بۆ بەکارهێنان. + سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە. + دۆمەینی نادروست تێنووسکرا + ئەمە ناتوانێت بەتاڵ بێت. + هەڵەیەک لە تۆڕ ڕوویدا! تکایە پەیوەندیت بپشکنە و دوبارە هەوڵ بدە! + هەڵەیەک ڕوویدا. + تایبەتمەندی بابەت گریمانەیی + دەرگای پرۆکسی HTTP + ڕاژەکاری پرۆکسی HTTP + چالاککردنی پرۆکسی HTTP + HTTP proxy + پرۆکسی + داگرتنی پێشبینینی میدیا + وەڵامدانەوەکان پیشان بدە + پیشاندانی بەهێزکردنەکان + سەرخشتەکان + فلتەرکردنی تایملاین + نمرەی لاری ڕەنگاوڕەنگ نیشان بدە بۆ میدیای شاراوە + وێنۆجکەی ئەنیمەی GIF + نیشاندەر نیشاندەر بۆ بۆتەکان نیشان بدە + زمان + دوگمەی ئاوازدانان بشارەوە لەکاتی خشاندن + بەکارهێنانی خشتەبەندەکانی دڵخواز + وێبگەڕ + دیزاینی سیستەم بەکاربهێنە + خۆکار لە کاتی خۆرئاوابووندا + ڕەش + ڕووناکی + تاریک + فلتەرەکان + ڕووکاری ئەپ + تایملاین + دەرکەوتن + کەسێک کە من بەشدارم لە بڵاو کردنەوەی توتێکی نوێیرکری + ڕاپرسی کۆتایی هاتووە + بابەتەکانی من پەسەندن + پۆستەکانم بەرزدەکرانەوه + بەدواداچوونەوەی داواکراو + بەدوادا + ناوبراو + ئاگادارم بکەوە کاتێک + ئاگاداربکەوە بە ڕووناکی + ئاگادارکردنەوەی لەلەرینە + ئاگادارکردنەوەی بە دەنگێک + ئاگادارکردنەوەکان + ئاگانامەکان + ئاگانامەکان + ڕاستەوخۆ: تەنها بۆ بەکارهێنەرانی ناوبراو پۆست بکە + تەنها شوێنکەوتوانی: تەنها پۆست بۆ شوێنکەوتوانی + لیستی نەکراو: لە هێڵی کاتی گشتی دا پیشان مەدە + گشتی: پۆست بکە بۆ هێڵی کاتی گشتی + شاردنەوەی ئاگانامەکان + بێدەنگکردن @%s؟ + بلۆککردنی @%s؟ + شاردنەوەی هەموو دۆمەینەکە + ئایا دڵنیایت لەوەی دەتەوێت هەموو %s بلۆک بکەیت؟ تۆ ناوەڕۆکێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان لە ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن. + ئەم دووانە بسڕەوە و دووبارە ڕەشنووس یان دەکەیتەوە؟ + ئەم توتە بسڕەوە؟ + شوێن نەکەوتنی ئەم هەژمارە؟ + داواکاری بەدوادا چوەکان هەڵوەشانەوە؟ + داگرتن + بارکردن… + تەواوکردنی بارکردنی میدیا + ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک فرەتر! +\n +\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. +\n +\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. +\n +\nزانیاری زیاتر دەتوانرێت بدۆزرێتەوە لە joinmastodon.org. + گرێدان… + نموونەیەک چییە؟ + سەرپەڕە + وێنۆچکە + وەڵام… + هیچ ئەنجامێک نیە + گەڕان… + دەربارە + ناوی پیشاندان + ئاگاداری ناوەڕۆک + گفتوگۆی بێدەنگ + + %d کاژێرماوە + %d کاژێرماوە + + کاتێک وشەکە یان دەستەواژەکە تەنها ئەبجەدییە، تەنها ئەگەر لەگەڵ هەموو وشەکە یەکبێت کاری پێدەکرێت + ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + شاردنەوەی زانیاری چەندێتی لەسەر پرۆفایلەکان + شاردنەوەی زانیاری چەندێتی لە بابەتەکان + سنووردارکردنی ئاگانامەکانی تایم لاین + پێداچوونەوەی ئاگانامەکان + هەندێک زانیاری کە لەوانەیە کاریگەری لەسەر باشبوونی دەروونیت دروست بکات دەشاردرێنەوە. ئەمە پێکدێت لە: +\n +\n- ئاگانامەکانی پەسەند/بەهێزکردن/بەدوادا +\n - پەسەندترین/بەرزکردنەوە لەسەر توت +\n - بەدواداچوون/زانیاری بابەت لەسەر پرۆفایلەکان +\n +\nکارتێکردنی ئاگانامەکانی پاڵپێوەنان، بەڵام دەتوانیت بە پەسەندکردنە ئاگانامەکانت دا بخشێنیەوە بە دەستی. + ڕزگارکرا + تێبینی تایبەتی تۆ دەربارەی ئەم ئەژمێرە + Wellbeing + شاردنەوەی ناونیشانی شریتی ئامڕازی سەرەوە + پیشاندانی دیالۆگی دووپاتکردنەوە پێش بەهێزکردن + نیشاندانی پێشاندانی بەستەر لە هێڵی کات + ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک. + هیچ ڕاگه یه نراوێک له بەرده رنه کەون. + هیچ بارێکی خشتەکراوت نیە. + هیچ ڕەشنووسێکت نییە. + هەڵە لە گەڕان بەدوای بابەت %s + دەستکاریکردن + هەڵبژاردنی %d + چەند هەڵبژاردنێک + زیادکردنی هەڵبژاردن + ڕاپرسی + چالاککردنی ئاماژەکردنی لێدانی چالاک بۆ گۆڕین لە نێوان خشتەبەندەکان + تاسکی کۆد و سەرمایەکانی تێدایە لەم پڕۆژە کراوەی سەرچاوە: + فلتەری ئاگانامەکان نیشان بدە + گەڕانەکە سەرکەوتوو نەبوو + ئەژمێرەکان + هەژمارەلە ڕاژەیەکی دیکەیە ترە. کۆپیەکی بێ سەروبەر بنێرە بۆ ڕاپۆرتەکە لەوێ؟ + ڕاپۆرتەکە دەنێردرێت بۆ بەڕێوەبەری ڕاژەکەت. دەتوانیت ڕوونکردنەوەیەک پێشکەش بکەیت کە بۆچی ئەم ئەژمێرە لە خوارەوە ڕاپۆرت دەکەیت: + سەرکەوتوو نەبوو لە هێنانی بارەکان + ڕاپۆرتکردن سەرکەوتوو نەبوو + ناردنەوە بۆ %s + سەرنجەکانی زیاتر + سەرکەوتووانە ڕاپۆرتکرا @%s + تەواوبوو + دواوە + بەردەوام بە + + %d چرکەی ماوەو + %d دووەم چەپ + + + %d خولەک ماوە + %d خولەک ماوە + + + %d ڕۆژ ماوە + %d ڕۆژ ماوە + + ڕاپرسییەک کە دروستت کردووە کۆتایی هات + ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات + دەنگ + داخراوە + کۆتایی دێت لە %s + + %s کەس + %s کەس + + + %s دەنگ + %s دەنگ + + %1$s • %2$s + کارەکان بۆ وێنە %s + ئایا دڵنیایت لەوەی دەتەوێت بە هەمیشەیی هەموو ئاگانامەکانت بسڕیتەوە؟ + دروستکردن + دروستکردنی توت + جێبەجێ کردن + فلتەر + سڕینەوە + لیست + دیاریکردنی لیست + هاشتاگی + هاشتاگی بێ # + هاشتاگی زیاد بکە + ناوی لیست + ڕاپرسی لەگەڵ هەڵبژاردنەکان: %1$s, %2$s, %3$s, %4$s; %5$s + ڕاستەوخۆ + شوێنکەوتوانی + لە لیست نەکراو + گشتی + نیشانکراوە + پەسەندکراو + دووبارە بڵاگ کرا + هیچ وەسفێک + ئاگاداری ناوەڕۆک: %s + میدیا: %s + بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + %1$s, %2$s و %3$d زیاتر + %1$s و %2$s + %1$s + پەسەندکراوە لەلایەن + بەرزکراوە لەلایەن + + %s بەهێزکردن + %s بەهێزکردن + + + %1$s دڵخواز + %1$s دڵخواز + + Pin + لابردن + ڕەنگە زانیاری خوارەوە ڕەنگدانەوەی پرۆفایلی بەکارهێنەر بە ناتەواوی بێت. فشار بکە بۆ کردنەوەی پرۆفایلی تەواو لە وێبگەڕەکە. + کاتی ڕەها بەکاربهێنە + ناوەڕۆک + ناونیشان + داتا زیاد بکە + مێتاداتای پرۆفایل + CC-BY-SA 4.0 + CC-BY 4.0 + مۆڵەتدراوە لەژێر مۆڵەتی ئەپاچی (لەبەرگیراوە لە خوارەوە) + بێ هێزکردن + بەرزکردنەوە بۆ جەماوەری ڕەسەن + %1$s گواسترایەوە بۆ: + بۆت + داگرتن سەرکەوتوو نەبوو + کۆمەڵە ئیمۆجیەکەی ئێستای گووگڵ + سێتی ئیمۆجی پێوانەیی ماتۆدۆن + ئیمۆجی Blob لە ئەندرۆید ەوە ناسراوە 4.4–7.1 + سێتی ئیمۆجی بنەڕەتی ئامێرەکەت + دەستپێکردنەوە + دواتر + تۆ پێویستە توسکی دەستپێبکەیتەوە بۆ ئەوەی ئەم گۆڕانکاریانە جێبەجێ بکەیت + دەسپێکردنەوەی کاربەرنامە پێویستە + کردنەوە توت + فراوانکردن/نوشتانەوەی هەموو بارەکان + ئەنجامدانی گەڕان… + تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت + سیستەمی بنەڕەت + شێوازی ئیمۆجی + ڕوونووسکراوە بۆ کلیپ بۆرد + نموونەکەت %s هیچ ئیمۆجییەکی ئاسایی نییە + دروستکردن + کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت + ناردنی هەڵوەشاوە + ناردنی توتس + هەڵە لە ناردنی توت + (توت) دەنێرم… + ڕەشنووس پاشەکەوت بکەیت؟ + داوات لێدەکات کە بە دەستی شوێنکەوتوانی پەسەند بکە + داخستنی ئەژمێر + لابردن + دانانی سەردێڕ + وەسف بکە بۆ بینایی داڕماو +\n(%d سنوری کاراکتەر) + دانانی سەردێڕ شکستی هێنا + بڵاوکردنەوە بە هەژماری %1$s + لابردنی ئەژمێر لە لیستەکە + زیادکردنی ئەژمێر بۆ لیستەکە + گەڕان بەدوای ئەو کەسانەی کە پەیڕەوی ان دەکەیت + دەستکاریکردنی لیستەکە + سڕینەوەی لیستەکە + ناونانەوەی لیستەکە + دروستکردنی لیستێک + نەیتوانی لیستەکە بسڕێتەوە + نەیتوانی ناوی لیست بنووسرێ + نەیتوانی لیست دروست بکات + لیستی تایم لاین + لیستەکان + لیستەکان + زیادکردنی ئەژمێری ماتۆدۆنی نوێ + زیادکردنی ئەژمێر + دەستەواژە بۆ فلتەر + هەموو وشەکە + نوێکردنەوە + لابردن + دەستکاریکردنی فلتەر + زیادکردنی فلتەر + گفتوگۆکان + هێڵی کاتی گشتی + بارکردنی زیاتر + وەڵام دانەوە بۆ @%s + میدیا + هەمیشە ئەو توتانەی کە بە ئاگادارکردنەوەکانی ناوەڕۆکەوە نیشانەکراون فراوان بکە + هەمیشە ناوەڕۆکی هەستیار نیشان بدە + دوای تۆ دەکەوێت + %ds + %dm + %dh + %dd + %dy + لە %ds + لە %dm + لە %dh + لە %dd + لە %dy + بەدواداچوونەوەی داواکراو + ڤیدیۆ + وێنەکان + هاوبەشکردنی لینک بۆ توت + هاوبەشکردنی ناوەڕۆکی دووت + پرۆفایلی تاسکی + ڕاپۆرتەکانی هەڵەکان و داواکاریەکانی تایبەتمەندی: +\nhttps://github.com/tuskyapp/Tusky/issues + وێبسایتی پڕۆژە: +\nhttps://tusky.app + توسکی سۆفتوێری ئازاد و سەرچاوەی کراوەیە مۆڵەتدراوە بە پێ نامەی گشتی GNU Public Version 3. دەتوانیت لێرە مۆڵەتەکە نیشان بدەی: https://www.gnu.org/licenses/gpl-3.0.en.html + لەلایەن تاسکیەوە دەست کراوە بە + توسکی %s + سەبارەت + هەژماری داخراو + %d چالاکی نوێ + %1$s و %2$s + %1$s و %2$s و %3$s + %1$s, %2$s, %3$s و %4$d ئەوانی تر + %s ئاماژەی بە تۆ کرد + ئاگانامەکان کاتێک کەسێک کە تۆ بەشداریت کردووە لە بڵاوکردنەوەی توتێکی نوێ + توتی نوێ + ئاگادارییەکان دەربارەی ڕاپرسییەکان کە کۆتایی هاتووە + ڕاپرسییەکان + ئاگانامەکان کاتێک کەتوتەکان نیشانە کراون وەک دڵخواز + دڵخوازەکان + ئاگانامەکان کاتێک کە دووتەکەت بەرز دەکرێتەوە + بەهێزکردن + ئاگانامەکان دەربارەی داواکاریەکانی بەدوادا + بەدواداچونی داواکاریەکان بکە + ئاگانامەکان دەربارەی شوێنکەوتوانی نوێ + شوێنکەوتوانی نوێ + ئاگانامەکان دەربارەی ئاماژە نوێیەکان + ئاماژە نوێیەکان + گەورەترین + گەورە + مامناوەندی + بچووک + بچووکترین + قەبارەی دەقی بار + شوێنکەوتوانی تەنها + لە لیست نەکراو + گشتی + خوارەوە + سەرەوە + شوێنی سەرەکی ڕێنیشاندەر + سەرکەوتوو نەبوو لە هاودەمکردنی ڕێکبەندەکان + بڵاوکردنەوە (هاوکاتکراوە لەگەڵ سێرڤەر) + هەمیشە میدیا وەک هەستیار نیشان بکە + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 29ef088a8..6f8de431a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -36,7 +36,7 @@ Blokovaní uživatelé Žádosti o sledování Upravit váš profil - Koncepty + Koncepty Licence \@%s %s boostnul/a @@ -422,13 +422,13 @@ Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii\? Zobrazit filtr oznámení Anketa - 5 minut - 30 minut - 1 hodinu - 6 hodin - 1 den - 3 dny - 7 dní + 5 minut + 30 minut + 1 hodinu + 6 hodin + 1 den + 3 dny + 7 dní Přidat možnost Lze zvolit více možností Možnost %d diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index c4f098e3e..29409f172 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -32,7 +32,7 @@ Defnyddwyr wedi\'u blocio Dilyn ceisiadau Golygu\'ch Proffil - Drafftiau + Drafftiau Trwyddedau %s wedi\'u hybu Cynnwys sensitif diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 65592e3ff..295362090 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,7 +36,7 @@ Blockierte Profile Folgeanfragen Dein Profil bearbeiten - Entwürfe + Entwürfe Lizenzen \@%s %s teilte @@ -395,13 +395,13 @@ Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie des Berichts auch dorthin geschickt werden\? Benachrichtigungsfilter anzeigen Umfrage - 5 Minuten - 30 Minuten - 1 Stunde - 6 Stunden - 1 Tag - 3 Tage - 7 Tage + 5 Minuten + 30 Minuten + 1 Stunde + 6 Stunden + 1 Tag + 3 Tage + 7 Tage Editieren test %s Umfrage hinzufügen @@ -470,4 +470,22 @@ Titel der Hauptnavigation verstecken Im Moment gibt es keine Ankündigungen. Ankündigungen + Der Beitrag auf den du antworten willst wurde gelöscht + Entwurf gelöscht + Alte Entwürfe + Das \"Entwürfe\"-Feature in Tusky wurde komplett neu gestaltet um schneller und benutzerfreundlicher zu sein. +\nDu kannst deine alten Entwürfe noch hinter einem Button bei den neuen Entwürfen finden, aber sie werden mit einem zukünftigen Update gelöscht! + Dieser Beitrag konnte nicht gesendet werden! + Willst du die Liste %s wirklich löschen\? + Du kannst nicht mehr als %1$d Anhänge hochladen. + Wohlbefinden + Dauer + Für immer + 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 + %s hat gerade etwas gepostet diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index b45c98472..906fdbb0e 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -36,7 +36,7 @@ Blokitaj uzantoj Petoj de sekvado Redakti vian profilon - Malnetoj + Malnetoj Permesiloj \@%s %s diskonigis @@ -407,13 +407,13 @@ Aldoni baloton Ĉiam pligrandigi tootoj markiĝita per enhavaj avertoj Baloto - 5 minutoj - 30 minutoj - 1 horo - 6 horoj - 1 tago - 3 tagoj - 7 tagoj + 5 minutoj + 30 minutoj + 1 horo + 6 horoj + 1 tago + 3 tagoj + 7 tagoj Aldoni elekton Multaj elektoj Elekton %d diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 637e47438..3489faebb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -36,7 +36,7 @@ Bloqueados Solicitudes Editar tu perfil - Borradores + Borradores Licencias \@%s %s compartió @@ -420,13 +420,13 @@ Error al buscar Añadir encuesta Encuesta - 5 minutos - 30 minutos - 1 hora - 6 horas - 1 día - 3 días - 7 días + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 día + 3 días + 7 días Añadir opción Opciones múltiples Opción %d @@ -479,4 +479,33 @@ Tu nota privada acerca de esta cuenta No hay anuncios. Anuncios + %s recién publicado + No puedes cargar más de %1$d archivos adjuntos multimedia. + Esconder las estadísticas cuantitativas de los perfiles + Esconder las estadísticas cuantitativas de las publicaciones + Revisar Notificaciones + Bienestar + Notificaciones cuando alguien al que estoy suscrito publicó un nuevo toot + Nuevos toots + alguien al que estoy suscrito publicó un nuevo toot + Algunas informaciones que podríam afectar tu bienestar van a ser ocultas. Esto incluye: +\n +\n- Notificaciones de favoritos, impulsos e seguidores +\n- Conteo de favoritos e impulsos en toots +\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 + Borrador eliminado + Error al cargar la información de respuesta + Borradores antiguos + La función de borrador en Tusky se ha rediseñado por completo para que sea más rápida, más fácil de usar y con menos errores. +\nAún puede acceder a sus borradores antiguos a través de un botón en la pantalla de borradores nuevos, ¡pero se eliminarán en una actualización futura! + ¡Este toot no se pudo enviar! + ¿Realmente quieres eliminar la lista %s\? + Indefinido + Duración + Adjuntos + Audio + Limitar notificaciones de cronología diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 0d7f5b56c..2aa1ab7a2 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -32,7 +32,7 @@ Blokeatuak Eskakizunak Profila editatu - Zirriborroak + Zirriborroak Lizentziak %s-(e)k bultzatu du Kontuz edukiarekin @@ -417,13 +417,13 @@ Bilaketa huts egin du Erakutsi jakinarazpenen iragazkia Inkesta - 5 minutu - 30 minutu - Ordu 1 - 6 ordu - Egun 1 - 3 egun - 7 egun + 5 minutu + 30 minutu + Ordu 1 + 6 ordu + Egun 1 + 3 egun + 7 egun Gehitu aukera Aukera anitzak %d. aukera diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 54470e167..c95164587 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -32,7 +32,7 @@ کاربران مسدود درخواست‌های پی‌گیری ویرایش نمایه‌تان - پیش‌نویس‌ها + پیش‌نویس‌ها پروانه‌ها %s تقویت کرد محتوای حسّاس @@ -400,13 +400,13 @@ شکست در جست‌وجو نمایش پالایهٔ آگاهی‌ها نظرسنجی - ۵ دقیقه - ۳۰ دقیقه - ۱ ساعت - ۶ ساعت - ۱ روز - ۳ روز - ۷ روز + ۵ دقیقه + ۳۰ دقیقه + ۱ ساعت + ۶ ساعت + ۱ روز + ۳ روز + ۷ روز افزودن گزینه گزینه‌های چندگانه گزینهٔ %d diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 14c99e673..0ef8b32b4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -36,7 +36,7 @@ Comptes bloqués Demandes d’abonnement Modifier votre profil - Brouillons + Brouillons Licences \@%s %s a partagé @@ -94,7 +94,7 @@ Mentionner Cacher les médias Ouvrir le menu - Sauvegarder + Enregistrer Modifier le profil Modifier Annuler @@ -425,13 +425,13 @@ Toujours ouvrir les pouets avec un contenu sensible Ajouter un sondage Sondage - 5 minutes - 30 minutes - 1 heure - 6 heures - 1 jour - 3 jours - 7 jours + 5 minutes + 30 minutes + 1 heure + 6 heures + 1 jour + 3 jours + 7 jours Ajouter un choix Choix multiples Choix %d @@ -488,4 +488,21 @@ 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 : +\n +\n - des notifications de favoris, de partage et de suivi +\n - du compte des favoris/partages sur les pouets +\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 à laquelle je suis abonné a publié un nouveau pouet + %s vient de publier + Examiner les notifications + Nouveau pouets + 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 + Cacher les statistiques quantitatives sur les profils + Cacher les statistiques quantitatives sur les publications diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index fa8b85627..130f4abef 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -177,7 +177,7 @@ Roghanna Cuntais Sainroghanna Logáil Amach - Dréachtaí + Dréachtaí Roghaí Theip ar fhíordheimhniú leis an gcás sin. Cad is sampla ann\? @@ -445,13 +445,13 @@ Taispeáin scagaire Fógraí Cumasaigh gotha swipe aistriú idir cluaisíní Vótaíocht - 5 nóiméad - 30 nóiméad - 1 uair an chloig - 6 uair an chloig - 1 lá - 3 lá - 7 lá + 5 nóiméad + 30 nóiméad + 1 uair an chloig + 6 uair an chloig + 1 lá + 3 lá + 7 lá Cuir rogha leis Ilroghanna Rogha %d diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index ed594dc7e..83bfce331 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -8,7 +8,7 @@ Roighainnean cunntais Roighainnean Clàraich a-mach - Dreachd + Dreachd Prìomhaich Dè a th ’ann an àite\? Deasaich diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 6c652ead9..3d4fe2c33 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2,9 +2,9 @@ हिंदी पसंदीदा - प्रारूप + प्रारूप लॉग आउट - पसंद + प्राथमिकताएं खाता प्राथमिकताएं प्रोफाइल एडिट करें खोज @@ -200,13 +200,13 @@ विकल्प जोड़ें विकल्प %d कई विकल्प - 7 दिन - 3 दिन - 1 दिन - 6 घंटे - 1 घंटा - 30 मिनिट - 5 मिनट + 7 दिन + 3 दिन + 1 दिन + 6 घंटे + 1 घंटा + 30 मिनिट + 5 मिनट %d घंटा शेष %d घंटे शेष @@ -403,4 +403,6 @@ %d सेकेंड शेष %d सेकेंड शेष + पोस्ट बहुत लंबा है! + उस सर्वर से प्रमाणित करने में विफल। \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d60fd544a..7f4990752 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -36,7 +36,7 @@ Letiltott felhasználók Követési kérelmek Profilod szerkesztése - Piszkozatok + Piszkozatok Licenszek \@%s %s megtolta @@ -237,12 +237,12 @@ Listák Törlés Fiók zárolása - Elmented a vázlatot? + 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 vázlataid közé + 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 @@ -417,13 +417,13 @@ Sikertelen keresés Szavazás hozzáadása Szavazás - 5 perc - 30 perc - 1 óra - 6 óra - 1 nap - 3 nap - 7 nap + 5 perc + 30 perc + 1 óra + 6 óra + 1 nap + 3 nap + 7 nap Válasz hozzáadása Több lehetőség Válasz %d @@ -442,7 +442,7 @@ Lista kiválasztása Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. - Nincs egy vázlatod sem. + Nincs egy piszkozatod sem. Nincs egy ütemezett tülköd sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek @@ -476,4 +476,34 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények + A Tülköt, melyre válaszul piszkozatot készítettél törölték + Piszkozat törölve + Nem sikerült a Válasz információit betölteni + Régi Piszkozatok + A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb és hibamentesebb legyen. +\nTovábbra is elérheted a régi piszkozataidat egy gombbal az új piszkozatok képernyőjén, de ezeket egy későbbi frissítésben el fogjuk törölni! + 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. + Profilok mérőszámainak elrejtése + Tülkök mérőszámainak elrejtése + Idővonali értesítések korlátozása + Értesítések Áttekintése + Pár információ, ami befolyásolhatja a mentális egészségedet rejtve marad. Ilyenek pl.: +\n +\n - Kedvenc/Megtolás/Bekövetés értesítései +\n - Kedvenc/Megtolás számlálók a tülkökön +\n - Követő/Tülk statisztikák a profilokon +\n +\nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat. + Végtelen + Időtartam + Csatolmányok + Audio + Értesítések általam követett személy új tülkjeiről + Új tülkök + valaki, akit követek újat tülkölt + %s épp tülkölt + Jóllét + Egyedi emojik animálása diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 4e27ac529..7f911d96d 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ Skrá inn með Mastodon Hvað er tilvik\? Eftirlæti - Drög + Drög Skrá út Kjörstillingar Eiginleikar tengingar @@ -396,13 +396,13 @@ Tókst ekki að leita Birta tilkynningasíu Athuga - 5 mínútur - 30 mínútur - 1 klukkustund - 6 klukkustundir - 1 dagur - 3 dagar - 7 dagar + 5 mínútur + 30 mínútur + 1 klukkustund + 6 klukkustundir + 1 dagur + 3 dagar + 7 dagar Bæta við valkosti Margir valkostir Valkostur %d diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7ccd04276..2b31d3e38 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -36,7 +36,7 @@ Utenti bloccati Richieste di seguirti Modifica il tuo profilo - Bozze + Bozze Licenze \@%s %s ha boostato @@ -431,13 +431,13 @@ Errore durante la ricerca Mostra il filtro delle notifiche Sondaggio - 5 minuti - 30 minuti - 1 ora - 6 ore - 1 giorno - 3 giorni - 7 giorni + 5 minuti + 30 minuti + 1 ora + 6 ore + 1 giorno + 3 giorni + 7 giorni Aggiungi scelta Scelte multiple Scelta %d @@ -468,7 +468,7 @@ La tua nota privata su questo account Nascondi il titolo della barra degli strumenti in alto Mostra la finestra di dialogo di conferma prima del boosting - Mostra le anteprime dei collegamenti nelle sequenze temporali + 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. @@ -483,4 +483,14 @@ Riattiva le notifiche da %s Annunci Richieste di seguirti + Nascondi statistiche quantitative sui profili + Nascondi le statistiche quantitative sui post + Limita le notifiche della timeline + Revisiona le notifiche + Benessere + Notifiche di quando qualcuno a cui sei iscritto ha pubblicato un nuovo toot + Nuovi toots + qualcuno a cui sono iscritto ha pubblicato un nuovo toot + %s appena pubblicato + Non puoi caricare più di %1$d allegati multimediali. \ 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 4f1ace36d..a5912d23b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -35,7 +35,7 @@ ブロックしたユーザー フォローリクエスト プロフィールを編集 - 下書き + 下書き ライセンス %sさんがブーストしました 閲覧注意 @@ -380,13 +380,13 @@ 参加した投票の結果がでました 作成した投票の結果がでました 投票 - 5分 - 30分 - 1時間 - 6時間 - 1日 - 3日 - 7日 + 5分 + 30分 + 1時間 + 6時間 + 1日 + 3日 + 7日 選択肢を追加 複数選択可 編集 diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 3c10f80b4..ad5a99d92 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -2,7 +2,7 @@ Qqen ɣer Maṣṭudun Ismenyifen - Irewwayen + Irewwayen Ffeɣ Iɣewwaṛen Iɣewwaṛen n umiḍan @@ -196,13 +196,13 @@ Tella-d tuccḍa deg cetki Tucḍa n unadi Assenqed - 5 n tisdidin - 30 n tisdidin - 1 n usrag - 6 n isragen - 1 n wass - 3 n wussan - 7 n wussan + 5 n tisdidin + 30 n tisdidin + 1 n usrag + 6 n isragen + 1 n wass + 3 n wussan + 7 n wussan Tafrant %d Ig ṭṭafar Imeḍfaṛen diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4689c5afa..893b4acb1 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -37,7 +37,7 @@ 숨긴 도메인 팔로우 요청 프로필 편집 - 임시 저장 + 임시 저장 라이선스 \@%s %s님이 부스트 했습니다 @@ -409,13 +409,13 @@ 열람주의로 설정된 툿을 항상 펼치기 투표 추가 투표 - 5분 - 30분 - 1시간 - 6시간 - 1일 - 3일 - 7일 + 5분 + 30분 + 1시간 + 6시간 + 1일 + 3일 + 7일 항목 추가 여러 항목 선택 가능 %d번 항목 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 72119eef6..4f4a7868b 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,7 +3,7 @@ മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\? പ്രിയപ്പെട്ടവ - കരടുകൾ + കരടുകൾ പുറത്തിറങ്ങുക മുൻഗണനകൾ അക്കൗണ്ട് മുൻഗണനകൾ @@ -111,4 +111,5 @@ അറിയിപ്പുകൾ ടാബുകൾ അറിയിപ്പുകൾ + പ്രഖ്യാപനങ്ങൾ \ 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 a1b4f7fa9..e86d26d4a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -36,7 +36,7 @@ Geblokkeerde gebruikers Volgverzoeken Profiel bewerken - Concepten + Concepten Licenties \@%s %s boostte @@ -418,13 +418,13 @@ Meldingenfilter tonen Heel woord Wanneer het trefwoord of zinsdeel alfanumeriek is, wordt het alleen gefilterd wanneer het hele woord overeenkomt - 5 minuten - 30 minuten - 1 uur - 6 uur - 1 dag - 3 dagen - 7 dagen + 5 minuten + 30 minuten + 1 uur + 6 uur + 1 dag + 3 dagen + 7 dagen Voeg keuze toe Meerdere keuzes Keuze %d diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index cbdb4fedc..44e1ec5fe 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -36,7 +36,7 @@ Blokkerte brukere Forespørsler om følgen Endre profilen din - Kladder + Kladder Lisenser \@%s %s boosted @@ -408,13 +408,13 @@ Ekspander alltid toots markert med innholdsadvarsel Legg til avstemming Avstemming - 5 minutter - 30 minutter - 1 time - 6 timer - 1 dag - 3 dager - 7 dager + 5 minutter + 30 minutter + 1 time + 6 timer + 1 dag + 3 dager + 7 dager Legg til valg Flere valg Valg %d @@ -467,4 +467,36 @@ Ditt private notat om denne kontoen Det er ingen kunngjøringer. Kunngjøringer + Skjul kvantitativ informasjon på profiler + Skjul kvantitativ informasjon på toots + Begrens tidslinjevarsler + Se over varsler + 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 +\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 + %s tootet akkurat + Du kan ikke laste opp flere enn %1$d mediavedlegg. + Uendelig + Varighet + Er du sikker på at du vil slette listen %s\? + Vedlegg + Lyd + Tootet du kladdet et svar til har blitt fjernet + Kladd slettet + Lasting av svarinformasjon feilet + Gamle kladder + KladdfunksjonaLiteten i Tusky er skrevet om og er nå kjappere, mer brukervennlig, og med færre feil. +\nGamle kladder er fortsatt tilgjengelige via en knapp på den nye kladdskjermen, men de vil bli fjernet i en fremtidig oppdatering! + Sending av toot feilet! + Animer egendefinerte emojis + Avslutt abonnementet + Abonner \ 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 7b40723bb..9e5cabb88 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -31,7 +31,7 @@ Utilizaires blocats Demandas d’abonament Modificar lo perfil - Borrolhons + Borrolhons Licéncias %s partejat Contengut sensible @@ -419,13 +419,13 @@ Fracàs de la recèrca Ajustar un sondatge Sondatge - 5 minutas - 30 minutas - 1 ora - 6 oras - 1 jorn - 3 jorns - 7 jorns + 5 minutas + 30 minutas + 1 ora + 6 oras + 1 jorn + 3 jorns + 7 jorns Ajustar d’opcions Opcions multiplas Opcion %d diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-pa/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 89bd1cd77..7ea8ebd65 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,7 @@ Zablokowani użytkownicy Prośby o możliwość śledzenia Edytuj profil - Szkice + Szkice Licencje %s podbił Wrażliwe treści @@ -430,13 +430,13 @@ Wyszukiwanie nie powidło się Pokaż filtr powiadomień Głosowanie - 5 minut - 30 minut - 1 godzina - 6 godzin - 1 dzień - 3 dni - 7 dni + 5 minut + 30 minut + 1 godzina + 6 godzin + 1 dzień + 3 dni + 7 dni Dodaj wybór Kilka wyborów Opcja %d diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index be2166433..bb91e7fbe 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -34,7 +34,7 @@ Usuários bloqueados Seguidores pendentes Editar perfil - Rascunhos + Rascunhos Licenças %s deu boost Mídia sensível @@ -236,7 +236,7 @@ %dh %dm %ds - Te segue + te segue Sempre mostrar mídia sensível Mídia Respondendo @%s @@ -417,13 +417,13 @@ Contas Erro ao pesquisar Enquete - 5 minutos - 30 minutos - 1 hora - 6 horas - 1 dia - 3 dias - 7 dias + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 dia + 3 dias + 7 dias Adicionar opção Múltiplas opções Opção %d @@ -472,4 +472,36 @@ Silencie notificações de %s Dessilencie notificações de %s Ocultar o título da barra superior de tarefas + 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 + Rascunho excluído + A função de rascunhos no Tusky foi totalmente redesenhada para ser mais rápida, mais fácil e com menos erros. +\nÉ possível acessar rascunhos antigos através de um botão na tela de novos rascunhos, mas serão removidos numa futura atualização! + Rascunhos antigos + Não é possível anexar mais de %1$d arquivos de mídia. + Ocultar status dos perfis + Ocultar status dos toots + Limitar notificações da linha do tempo + Revisar notificações + Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: +\n +\n- Notificações de favoritos, boosts e seguidores +\n- Número de favoritos e boosts nos toots +\n- Status de toots e seguidores nos perfis +\n +\nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente. + Salvo! + Nota pessoal sobre esta conta aqui + Bem-estar + Sem comunicados. + Indefinido + Duração + Anexos + Áudio + Novos toots + %s recém tootou + Comunicados diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0c4830ddf..57c9d4753 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -36,7 +36,7 @@ Список блокировки Запросы на подписку Редактировать профиль - Черновики + Черновики Лицензии \@%s %s продвинул(а) @@ -444,13 +444,13 @@ Аккаунты Поиск завершился ошибкой Опрос - 5 минут - 30 минут - 1 час - 6 часов - 1 день - 3 дня - 7 дней + 5 минут + 30 минут + 1 час + 6 часов + 1 день + 3 дня + 7 дней Добавить Множественный выбор Вариант %d @@ -505,4 +505,16 @@ Скрыть заголовок в верхней панели Объявлений нет. Объявления + "Некоторая информация, которая может повлиять на ваше психическое благополучие, будет скрыта. Это включает в себя: +\n +\n - Избранное/Продвижение/Уведомления подписок +\n - Избранное/Продвижение счета на тутах +\n - Статистика подписчиков/публикаций в профилях +\n +\n На push-уведомления это не повлияет, но вы можете просмотреть настройки уведомлений вручную." + Благосостояние + Неопределённая + Продолжительность + Вложения + Аудио \ No newline at end of file diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index a322838e8..18bb76015 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -36,7 +36,7 @@ \@%s अनुज्ञापत्राणि कालबद्धदौत्यानि - लेखविकर्षाः + लेखविकर्षाः स्वीयव्यक्तिविवरणं सम्पाद्यताम् अनुसरणार्थमनुरोधाः प्रच्छन्नप्रदेशाः @@ -373,13 +373,13 @@ मतम् %d बहूनि मतानि अपरं मतं युज्यताम् - ७ दिनानि - ३ दिनानि - १ दिनम् - ६ घण्टाः - १ घण्टा - ३० निमेषाः - ५ निमेषाः + ७ दिनानि + ३ दिनानि + १ दिनम् + ६ घण्टाः + १ घण्टा + ३० निमेषाः + ५ निमेषाः मतपेटिका सारणहावभावस्य संयुतनं पीठिकापरिवर्तनार्थं कार्यम् सूचनाशोधकं दृश्यताम् diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 68c278706..14af6584d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -1,7 +1,7 @@ Prihlásiť sa účtom Mastodon - Koncepty + Koncepty Odhlásiť sa Nastavenia Nastavenia účtu diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 97ada28c7..0080993ae 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -34,7 +34,7 @@ Blokirani uporabniki Zahteve za Sledenje Uredi svoj profil - Osnutki + Osnutki Licence \@%s Občutljiva vsebina @@ -415,13 +415,13 @@ Vedno razširite tute, označene z opozorilom o vsebini Dodaj anketo Anketa - 5 minut - 30 minut - 1 ura - 6 ur - 1 dan - 3 dni - 7 dni + 5 minut + 30 minut + 1 ura + 6 ur + 1 dan + 3 dni + 7 dni Dodaj izbiro Več izbir Izbira %d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3907fae3c..618c18469 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -36,7 +36,7 @@ Blockerade användare Följarförfrågningar Ändra din profil - Utkast + Utkast Licenser \@%s %s knuffade @@ -425,13 +425,13 @@ Sökning misslyckades Skapa en omröstning Omröstning - 5 minuter - 30 minuter - 1 timme - 6 timmar - 1 dag - 3 dagar - 7 dagar + 5 minuter + 30 minuter + 1 timme + 6 timmar + 1 dag + 3 dagar + 7 dagar Lägg till alternativ Flerval Val %d @@ -484,4 +484,20 @@ Din privata notering om detta kontot Det finns inga meddelanden. Meddelanden + Aviseringar när någon du följer skrivit en ny toot + Nya toots + någon som jag följer har skrivit en ny toot + %s skrev precis + Dölj kvantitativ information på profiler + Dölj kvantitativ information på inlägg + Begränsa tidslinje aviseringar + Ändra aviseringar + Information som kan påverka ditt välmående kommer att döljas. Detta inkluderar: +\n +\n- Favorisering/Knuff/Följaraviseringar +\n- Favorisering/Antal knuffar +\n- Följare/Inlägg på profiler +\n +\nPush-aviseringar påverkas inte, men du ändra dina aviseringinställningar manuellt. + Välmående \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f0dc13795..19b82709d 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -29,7 +29,7 @@ தடைசெய்யபட்ட பயனர்கள் பின்பற்ற கோரிக்கை சுயவிவரத்தை திருத்த - வரைவுகள் + வரைவுகள் %s மேலேற்றப்பட்டது உணர்ச்சிகரமான உள்ளடக்கம் ஊடகம் மறைக்கப்பட்டது @@ -265,9 +265,9 @@ நேரடி தகவல் பட்டைகள் பொருத்தப்பட்டது - 1 நாள் - 3 நாட்கள் - 7 நாட்கள் + 1 நாள் + 3 நாட்கள் + 7 நாட்கள் விருப்பத்தைச் சேர் பின்பற்ற கோரிக்கை நீக்கு diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-te/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ea831d516..fb5bd52b5 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1,13 +1,13 @@ เพิ่มตัวเลือก - 7 วัน - 3 วัน - 1 วัน - 6 ชั่วโมง - 1 ชั่วโมง - 30 นาที - 5 นาที + 7 วัน + 3 วัน + 1 วัน + 6 ชั่วโมง + 1 ชั่วโมง + 30 นาที + 5 นาที โพล เปิดใช้งานการเลื่อนนิ้วเพื่อสลับระหว่างแท็บ แสดงตัวกรองการแจ้งเตือน @@ -431,7 +431,7 @@ ตั้งค่าบัญชี ตั้งค่า ออกจากระบบ - ฉบับร่าง + ฉบับร่าง ชื่นชอบ การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว Instance คือ\? @@ -455,4 +455,37 @@ ซ่อนการแจ้งเตือน ปิดเสียงการแจ้งเตือนจาก %s ซ่อนหัวข้อของแถบเครื่องมือด้านบน + ล้มเหลวในการส่งโพสต์นี้! + ข้อมูลบางอย่างที่อาจส่งผลต่อสุขภาพจิตของคุณจะถูกซ่อนไว้ซึ่งรวมถึง: +\n +\n- การแจ้งเตือน ชื่นชอบ/ดัน/ติดตาม +\n- จำนวนการ ชื่นชอบ/ดัน บนโพสต์ +\n- สถิติ ผู้ติดตาม/โพสต์ ในโปรไฟล์ +\n +\n การแจ้งเตือนแบบพุชจะไม่ได้รับผลกระทบ แต่คุณสามารถตรวจสอบการตั้งค่าการแจ้งเตือนได้ด้วยตนเอง + แจ้งเตือน Limit timeline + แจ้งเตือน Review + ใครบางคนที่ฉันได้ติดตาม ได้เผยแพร่โพสต์ใหม่ + ฟีเจอร์ฉบับร่างใน Tusky ได้รับการออกแบบใหม่ทั้งหมดเพื่อให้เร็วขึ้นเป็นมิตรกับผู้ใช้มากขึ้นและบั๊กน้อยลง +\n คุณยังสามารถเข้าถึงฉบับร่างเก่าผ่านปุ่มในหน้าฉบับร่างใหม่ แต่จะถูกลบออกในการอัปเดตในอนาคต! + ซ่อนสถิติเชิงปริมาณในโปรไฟล์ + ซ่อนสถิติเชิงปริมาณของโพสต์ + สุขภาวะ + บันทึกส่วนตัวของคุณเกี่ยวกับบัญชีนี้ + แจ้งเตือน เมื่อคนที่คุณติดตาม ได้เผยแพร่โพสต์ใหม่ + โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว + ลบฉบับร่างแล้ว + ล้มเหลวในการโหลดข้อมูลตอบกลับ + ฉบับร่างเก่า + คุณต้องการลบลิสต์ %s ใช่ไหม\? + คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + บันทึกแล้ว! + ไม่มีประกาศ + ไม่มีกำหนด + ระยะเวลา + ไฟล์แนบ + เสียง + โพสต์ใหม่ + %s พึ่งโพสต์ + ประกาศ \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2d482f40b..8f223dba4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -36,7 +36,7 @@ Engellenmiş kullanıcılar Takip Etme İstekleri Profili düzeltme - Taslaklar + Taslaklar Lisanslar \@%s %s yineledi @@ -418,13 +418,13 @@ Hesaplar Arama başarısız Anket - 5 dakika - 30 dakika - 1 saat - 6 saat - 1 gün - 3 gün - 7 gün + 5 dakika + 30 dakika + 1 saat + 6 saat + 1 gün + 3 gün + 7 gün Seçenek ekle Çoklu seçim Düzenle diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cb7869add..5c49c828c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -37,7 +37,7 @@ Налаштування акаунта Налаштування Вийти - Чернетки + Чернетки Вподобане Увійти Зʼєднання… diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index e6a93e82c..f3aa8bfa9 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -124,8 +124,8 @@ Mở ngăn kéo Làm mờ hình ảnh Nhắc tới - Bỏ ẩn cuộc trò chuyện - Ẩn cuộc trò chuyện + Mở lại thông báo + Tắt thông báo Ẩn %s Bỏ ẩn Ẩn @@ -134,7 +134,7 @@ Tạo bình chọn Thêm tệp Mở trong trình duyệt - Bộ sưu tập + Album Yêu cầu theo dõi Máy chủ đã ẩn Người dùng đã chặn @@ -171,7 +171,7 @@ Thu gọn Xem thêm Thu gọn - Mở rộng + Xem thêm Hiển thị Nội dung bị ẩn Nhạy cảm @@ -188,7 +188,7 @@ Người theo dõi Theo dõi Ghim - Tương tác + Rép Tút Tút Xếp tab @@ -197,7 +197,7 @@ Cộng đồng Thông báo Bảng tin - Nháp + Nháp Lượt thích Máy chủ là gì\? Tải xem trước hình ảnh @@ -289,7 +289,7 @@ Cộng đồng xem thêm Trả lời @%s - Bộ sưu tập + Thư viện Luôn hiện nội dung bị ẩn Luôn hiện nội dung nhạy cảm Đang theo dõi bạn @@ -318,13 +318,13 @@ Lựa chọn %d Cho phép chọn nhiều lựa chọn Thêm lựa chọn - 7 ngày - 3 ngày - 1 ngày - 6 giờ - 1 giờ - 30 phút - 5 phút + 7 ngày + 3 ngày + 1 ngày + 6 giờ + 1 giờ + 30 phút + 5 phút Bình chọn Vuốt qua lại giữa các tab Hiện bộ lọc thông báo @@ -352,7 +352,7 @@ Cuộc bình chọn bạn tạo đã kết thúc Cuộc bình chọn của bạn đã kết thúc Bình chọn - Kết thúc + xong kết thúc lúc %s %s người @@ -420,7 +420,7 @@ Để sau Bạn cần khởi động lại Tusky để áp dụng các thiết lập Yêu cầu khởi động lại ứng dụng - Mở tút + Xem tút Mở rộng/Thu gọn toàn bộ tút Đang tìm kiếm… Bạn cần tải về bộ emoji này trước @@ -456,7 +456,39 @@ Bỏ ẩn %s Ẩn tiêu đề tab Đã lưu! - Ghi chú của bạn + Thêm ghi chú Chưa có thông báo. - Tin tức + Có gì mới\? + Ẩn số liệu trên trang cá nhân + Ẩn tương tác trên tút + Hạn chế thông báo trên bảng tin + Chọn loại thông báo + Các thông tin ảnh hưởng tới tâm lý hành vi của bạn sẽ bị ẩn. Bao gồm: +\n +\n - Thông báo Lượt thích/Chia sẻ/Theo dõi +\n - Số Lượt thích/Chia sẻ của tút +\n - Số Người theo dõi/Tút trên trang cá nhân +\n +\nThông báo đẩy sẽ không ảnh hưởng, bạn có thể tự thiết lập trong phần cài đặt điện thoại của bạn. + Cai nghiện + Thông báo khi người bạn đăng ký theo dõi đăng tút mới + Tút mới + người tôi đăng ký theo dõi đăng tút mới + %s vừa đăng tút + Bạn không thể đính kèm quá %1$d tệp. + Vĩnh viễn + Thời hạn + Bạn thật sự muốn xóa danh sách %s\? + Đính kèm + Âm thanh + Tút bạn lên lịch đã bị hủy bỏ + Tút lên lịch cũ + Tút lên lịch đã xóa + Chưa tải được bình luận + Tính năng lên lịch đăng tút của Tusky được thiết kế lại hoàn toàn để nhanh hơn, thân thiện hơn và ít lỗi hơn. +\nBạn vẫn có thể xem lại bản nháp cũ nhưng chúng sẽ bị xóa bỏ trong bản cập nhật tương lai! + Đăng tút không thành công! + Emoji động + Ngưng nhận thông báo + Nhận thông báo \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bd147cc59..bdba6f0c7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -36,7 +36,7 @@ 被屏蔽的用户 关注请求 编辑个人资料 - 草稿 + 草稿 开源协议 \@%s %s 转嘟了 @@ -436,13 +436,13 @@ 搜索失败 显示通知过滤器 投票 - 5 分钟 - 30 分钟 - 1 小时 - 6 小时 - 1 天 - 3 天 - 7 天 + 5 分钟 + 30 分钟 + 1 小时 + 6 小时 + 1 天 + 3 天 + 7 天 添加选择 多项选择 选择 %d diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 706aac194..f02a62650 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1,24 +1,24 @@ - 應用程式出現異常 - 網絡請求出錯,請檢查互聯網連接並重試 - 內容不能為空 + 應用程式出現異常。 + 網絡請求出錯,請檢查互聯網連接並重試! + 內容不能為空。 該域名無效 - 無法連接此伺服器 - 沒有可用的瀏覽器 - 認證過程出現未知錯誤 - 授權被拒絕 - 無法獲取登入資訊 + 無法連接此伺服器。 + 沒有可用的瀏覽器。 + 認證過程出現未知錯誤。 + 授權被拒絕。 + 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB - 影片大小限制 40MB - 無法上傳此類型的檔案 - 此檔案無法開啟 - 需要授予 Yuito 讀取媒體檔案的權限 - 需要授予 Yuito 寫入儲存空間的權限 - 無法在嘟文中同時插入影片和圖片 - 媒體檔案上傳失敗 - 嘟文發送時出錯 + 檔案大小限制 8MB。 + 影片大小限制 40MB。 + 無法上傳此類型的檔案。 + 此檔案無法開啟。 + 需要授予 Yuito 讀取媒體檔案的權限。 + 需要授予 Yuito 寫入儲存空間的權限。 + 無法在嘟文中同時插入影片和圖片。 + 媒體檔案上傳失敗。 + 嘟文發送時出錯。 主頁 通知設定 本站時間軸 @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 @@ -47,10 +47,10 @@ 摺疊內容 展開 摺疊 - 沒有內容 - 還沒有內容,向下拉動即可重新整理 + 沒有內容。 + 還沒有內容,向下拉動即可重新整理! %s 轉嘟了你的嘟文 - %s 收藏了你的嘟文 + %s 把你的嘟文加入了最愛 %s 關注了你 檢舉使用者 @%s 的濫用行為 更多評論? @@ -112,12 +112,12 @@ 話題 打開轉嘟用戶主頁 顯示轉嘟 - 顯示收藏 + 顯示最愛 話題 提及 連結 打開媒體 #%d - 正在下載 %1$s… + 正在下載 %1$s 複製連結 打開為 %s 分享為 … @@ -130,8 +130,8 @@ 已解除封鎖 已解除靜音 已檢舉! - 成功送出回覆 - 域名 + 成功送出回覆。 + 哪一個域名? 有什麼新鮮事? 敏感內容警告 暱稱 @@ -143,8 +143,14 @@ 標題 什麼是站點? 正在連線… - 請輸入你帳號所在的 Mastodon 站點的域名或地址 - 正在完成上傳… + 輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 更多 +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 joinmastodon.org 查看。 + 正在完成上傳 正在上傳… 下載 移除關注請求? @@ -165,7 +171,7 @@ 被提及 有新的關注者 嘟文被轉嘟 - 嘟文被收藏 + 嘟文被加入收藏 投票已結束 外觀 佈景主題 @@ -210,7 +216,7 @@ 轉嘟 當有使用者轉嘟了我的嘟文時 收藏 - 當有使用者收藏了我的嘟文時 + 當有使用者把我的嘟文加入收藏時 投票 當我參與的投票結束時 %s 提及了你 @@ -323,7 +329,7 @@ 標籤 內容 嘟文顯示精確時間 - 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟 + 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。 取消置頂 置頂 @@ -332,8 +338,8 @@ <b>%s</b> 次轉嘟 - 轉嘟 - 收藏 + 轉嘟由 + 收藏由 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -402,4 +408,100 @@ 話題 關注請求 編輯 - \ No newline at end of file + 動態自訂表情符號 + 在隱藏的媒體上使用漸變色彩 + 動態 GIF 頭像 + 我關注的人有新嘟文 + 已送出關注請求 + 隱藏通知 + 靜音 @%s? + 封鎖 @%s? + 隱藏整個網域 + 確定要封鎖 %s 所有內容?你將不會在任何公開時間軸或是通知中看到來自這個網域的內容。你的關注者若來自這個網域則將會被移除。 + %s 已解除隱藏 + 重設 + 排程嘟文 + 排程的嘟文 + 取消靜音對話 + 靜音對話 + 取消靜音 %s + 靜音 %s + 靜音來自 %s 的通知 + 取消靜音來自 %s 的通知 + 取消靜音 %s + 新增投票 + 被隱藏的網域 + 被加入書籤 + 我的書籤 + 書籤 + 我的書籤 + %s 剛剛發了新嘟文 + %s 希望可以關注你 + 公告 + 已排程的嘟文 + 被隱藏的網域 + 聲音檔大小限制 40MB。 + 完整字詞 + 你的草稿欲回覆的原嘟文已被刪除 + 草稿已刪除 + 載入回覆資訊失敗 + 舊的草稿 + 這條嘟文發送失敗! + 你確定要刪除列表 %s? + 你無法上傳超過 %1$d 媒體附件。 + 已儲存! + 你對此帳號的個人註記 + 隱藏頂端工具列的標題 + 在轉嘟時提示確認 + 在時間軸中顯示連結預覽 + Mastodon 的最短發文間隔限制為 5 分鐘。 + 沒有公告。 + 你沒有任何已排程的嘟文。 + 你沒有任何草稿。 + 尋找嘟文時發生錯誤 %s + 選項 %d + 多個選項 + 新增選項 + 7 天 + 3 天 + 1 天 + 6 小時 + 1 小時 + 30 分鐘 + 5 分鐘 + 無限期 + 期間 + 投票 + 啟用在分頁間切換的滑動手勢 + 顯示通知過濾器 + 搜尋失敗 + 帳號 + 擷取狀態失敗 + 回報失敗 + 轉送至 %s + 額外的評論 + 成功回報 @%s + 完成 + 返回 + 繼續 + + %s 人 + + + 列表 + 選擇列表 + 加上話題標籤 + 投票選項: %1$s, %2$s, %3$s, %4$s; %5$s + Google 目前的表情符號包 + 總是顯示被標注為內容警告的嘟文 + 附件 + 錄音 + 由 Tusky 提供 + Tusky %s + 當你關注的人發布新嘟文時通知 + 新嘟文 + 關注請求的通知 + 底端 + 頂端 + 主要導覽列的位置 + diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 789bf966c..c60e11268 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 793fda6fa..c573716bd 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -36,7 +36,7 @@ 被屏蔽的用户 关注请求 编辑个人资料 - 草稿 + 草稿 开源协议 \@%s %s 转嘟了 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 681bdc6a1..cd264d1b5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,24 +1,24 @@ - 應用程式出現異常 - 網絡請求出錯,請檢查互聯網連接並重試 - 內容不能為空 + 應用程式出現異常。 + 網絡請求出錯,請檢查互聯網連接並重試! + 內容不能為空。 該域名無效 - 無法連接此伺服器 - 沒有可用的瀏覽器 - 認證過程出現未知錯誤 - 授權被拒絕 - 無法獲取登入資訊 + 無法連接此伺服器。 + 沒有可用的瀏覽器。 + 認證過程出現未知錯誤。 + 授權被拒絕。 + 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB - 影片大小限制 40MB - 無法上傳此類型的檔案 - 此檔案無法開啟 - 需要授予 Yuito 讀取媒體檔案的權限 - 需要授予 Yuito 寫入儲存空間的權限 - 無法在嘟文中同時插入影片和圖片 - 媒體檔案上傳失敗 - 嘟文發送時出錯 + 檔案大小限制 8MB。 + 影片大小限制 40MB。 + 無法上傳此類型的檔案。 + 此檔案無法開啟。 + 需要授予 Yuito 讀取媒體檔案的權限。 + 需要授予 Yuito 寫入儲存空間的權限。 + 無法在嘟文中同時插入影片和圖片。 + 媒體檔案上傳失敗。 + 嘟文發送時出錯。 主頁 通知 本站時間軸 @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 @@ -47,8 +47,8 @@ 摺疊內容 展開 摺疊 - 沒有內容 - 還沒有內容,向下拉動即可重新整理 + 沒有內容。 + 還沒有內容,向下拉動即可重新整理! %s 轉嘟了你的嘟文 %s 收藏了你的嘟文 %s 關注了你 @@ -112,12 +112,12 @@ 話題 打開轉嘟用戶主頁 顯示轉嘟 - 顯示收藏 + 顯示最愛 話題 提及 連結 打開媒體 #%d - 正在下載 %1$s… + 正在下載 %1$s 複製連結 打開為 %s 分享為 … @@ -130,8 +130,8 @@ 已解除封鎖 已解除靜音 已發送! - 成功送出回覆 - 域名 + 成功送出回覆。 + 哪一個域名? 有什麼新鮮事? 敏感內容警告 暱稱 @@ -143,8 +143,14 @@ 標題 什麼是站點? 正在連線… - 請輸入你帳號所在的 Mastodon 站點的域名或地址 - 正在完成上傳… + 輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 更多 +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 joinmastodon.org 查看。 + 正在完成上傳 正在上傳… 下載 移除關注請求? @@ -165,7 +171,7 @@ 被提及 有新的關注者 嘟文被轉嘟 - 嘟文被收藏 + 嘟文被加入收藏 投票已結束 外觀 佈景主題 @@ -210,7 +216,7 @@ 轉嘟 當有使用者轉嘟了我的嘟文時 收藏 - 當有使用者收藏了我的嘟文時 + 當有使用者把我的嘟文加入收藏時 投票 當我參與的投票結束時 %s 提及了你 @@ -323,7 +329,7 @@ 標籤 內容 嘟文顯示精確時間 - 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟 + 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。 取消置頂 置頂 @@ -333,7 +339,7 @@ <b>%s</b> 次轉嘟 轉嘟 - 收藏 + 收藏由 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -428,4 +434,92 @@ 編輯 書籤 音檔必需小於40MB。 - \ No newline at end of file + Tusky 的草稿功能已重新設計,更快、更好用、更少問題。 +\n 你還是可以在草稿頁面中查看你的先前的舊草稿,但它們在未來的某次更新中將會被移除! + 隱藏個人頁面中的狀態數量資訊 + 隱藏貼文上的狀態數量資訊 + 限制時間軸通知 + 檢查通知設定 + 有些資訊可能會影響你的心理健康將會被隱藏。包括: +\n +\n- 收藏/轉嘟/關注 通知 +\n- 收藏/轉嘟 數量 +\n- 關注/貼文 在個人頁面的狀態 +\n +\n推播通知不會受到影響,但你可以手動檢查你的通知設定。 + 數位健康 + + %s 人 + + + %s 剛剛發了新嘟文 + %s 請求關注你 + 動態自訂表情符號 + 你的草稿欲回覆的原嘟文已被刪除 + 草稿已刪除 + 載入回覆資訊失敗 + 舊的草稿 + 這條嘟文發送失敗! + 附件 + 錄音 + 你確定要刪除列表 %s? + 7 天 + 3 天 + 1 天 + 6 小時 + 1 小時 + 30 分鐘 + 5 分鐘 + 無限期 + 期間 + 你無法上傳超過 %1$d 媒體附件。 + 當你關注的人發布新嘟文時通知 + 新嘟文 + 我關注的人有新嘟文 + 沒有公告。 + 公告 + 已儲存! + 你對此帳號的個人註記 + 隱藏頂端工具列的標題 + 隱藏通知 + 靜音來自 %s 的通知 + 取消靜音來自 %s 的通知 + 取消靜音 %s + 取消靜音 %s + 底端 + 頂端 + 主要導覽列的位置 + 在隱藏的媒體上使用漸變色彩 + 加上話題標籤 + 在轉嘟時提示確認 + 在時間軸中顯示連結預覽 + 啟用在分頁間切換的滑動手勢 + 關注請求的通知 + 已送出關注請求 + 靜音 @%s? + 封鎖 @%s? + 取消靜音對話 + 靜音對話 + Mastodon 的最短發文間隔限制為 5 分鐘。 + 你沒有任何草稿。 + 你沒有任何已排程的嘟文。 + 列表 + 選擇列表 + 被加入書籤 + 我的書籤 + 書籤 + 由 Tusky 提供 + 尋找嘟文時發生錯誤 %s + 重設 + 排程嘟文 + 排程的嘟文 + 已排程的嘟文 + 選項 %d + 多個選項 + 新增選項 + 投票 + 新增投票 + 總是顯示被標注為內容警告的嘟文 + 搜尋失敗 + 帳號 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index dd4f599ca..5ec69307a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -45,6 +45,7 @@ 5dp 12dp + 120dp 72dp 108dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 81b4886bd..1beee3bf0 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -60,12 +60,15 @@ Taqbaylit Tiếng Việt Türkçe + български Русский العربية + کوردیی ناوەندی বাংলা (বাংলাদেশ) বাংলা (ভারত) فارسی हिंदी + संस्कृतम् தமிழ் ภาษาไทย 한국어 @@ -103,12 +106,15 @@ kab vi tr + bg ru ar + ckb bn-bd bn-in fa hi + sa ta th ko @@ -136,13 +142,13 @@ - @string/poll_duration_5_min - @string/poll_duration_30_min - @string/poll_duration_1_hour - @string/poll_duration_6_hours - @string/poll_duration_1_day - @string/poll_duration_3_days - @string/poll_duration_7_days + @string/duration_5_min + @string/duration_30_min + @string/duration_1_hour + @string/duration_6_hours + @string/duration_1_day + @string/duration_3_days + @string/duration_7_days @@ -155,5 +161,27 @@ 604800 + + @string/duration_indefinite + @string/duration_5_min + @string/duration_30_min + @string/duration_1_hour + @string/duration_6_hours + @string/duration_1_day + @string/duration_3_days + @string/duration_7_days + + + + 0 + 300 + 1800 + 3600 + 21600 + 86400 + 259200 + 604800 + + <b>%1$d%%</b> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00e6d22fe..0d2f58904 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ Hidden domains Follow Requests Edit your profile - Drafts + Drafts Scheduled toots Announcements Licenses @@ -268,6 +268,7 @@ Show indicator for bots Animate GIF avatars Show colorful gradients for hidden media + Animate custom emojis Timeline filtering Tabs @@ -355,6 +356,8 @@ Share link to toot Images Video + Audio + Attachments Follow requested @@ -589,13 +592,15 @@ Poll - 5 minutes - 30 minutes - 1 hour - 6 hours - 1 day - 3 days - 7 days + Duration + Indefinite + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days Add choice Multiple choices Choice %d @@ -612,6 +617,7 @@ Wellbeing Your private note about this account Saved! + Some information that might affect your mental wellbeing will be hidden. This includes:\n\n - Favorite/Boost/Follow notifications\n - Favorite/Boost count on toots\n @@ -623,5 +629,20 @@ Hide quantitative stats on posts Hide quantitative stats on profiles You cannot upload more than %1$d media attachments. + Do you really want to delete the list %s? + This toot failed to send! + + + The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy.\n + You can still access your old drafts via a button on the new drafts screen, + but they will be removed in a future update! + + Old Drafts + Failed loading Reply information + Draft deleted + The Toot you drafted a reply to has been removed + + Subscribe + Unsubscribe diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 7f33a9769..bd9be3b21 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -13,7 +13,6 @@ * 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.Intent @@ -25,6 +24,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.MediaUploader +import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account @@ -115,6 +115,7 @@ class ComposeActivityTest { accountManagerMock, mock(MediaUploader::class.java), mock(ServiceClient::class.java), + mock(DraftHelper::class.java), mock(SaveTootHelper::class.java), dbMock ) diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt index 73a00670c..b603a4a7c 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt @@ -44,6 +44,29 @@ class ComposeTokenizerTest(private val text: CharSequence, arrayOf(" @ment10n_ @ment20n_", 11, 20), arrayOf(" @ment10n_ @ment20n_n", 11, 21), arrayOf(" @ment10n_ @ment20n_9", 11, 21), + arrayOf(" @ment10n-", 1, 10), + arrayOf(" @ment10n- @", 11, 12), + arrayOf(" @ment10n- @ment20n", 11, 19), + arrayOf(" @ment10n- @ment20n-", 11, 20), + arrayOf(" @ment10n- @ment20n-n", 11, 21), + arrayOf(" @ment10n- @ment20n-9", 11, 21), + arrayOf("@ment10n@l0calhost", 0, 18), + arrayOf(" @ment10n@l0calhost", 1, 19), + arrayOf(" @ment10n_@l0calhost", 1, 20), + arrayOf(" @ment10n-@l0calhost", 1, 20), + arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35), + arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36), + arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36), + arrayOf(" @m@localhost", 1, 13), + arrayOf(" @m@localhost @a@localhost", 14, 26), + arrayOf("@m@", 0, 3), + arrayOf(" @m@ @a@asdf", 5, 12), + arrayOf(" @m@ @a@", 5, 8), + arrayOf(" @m@ @a@a", 5, 9), + arrayOf(" @m@a @a@m", 6, 10), + arrayOf("@m@m@", 5, 5), + arrayOf("#tusky@husky", 12, 12), + arrayOf(":tusky@husky", 12, 12), arrayOf("mention", 7, 7), arrayOf("ment10n", 7, 7), arrayOf("mentio_", 7, 7), diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index 1de9820a1..1d2ce9bec 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -28,6 +28,7 @@ import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config +import retrofit2.Response import java.util.* import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList @@ -76,8 +77,8 @@ class TimelineRepositoryTest { makeStatus("3"), makeStatus("2") ) - whenever(mastodonApi.homeTimelineSingle(isNull(), isNull(), anyInt())) - .thenReturn(Single.just(statuses)) + whenever(mastodonApi.homeTimeline(isNull(), isNull(), anyInt())) + .thenReturn(Single.just(Response.success(statuses))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -107,8 +108,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -141,8 +142,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -181,8 +182,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "3" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -224,8 +225,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "4" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -263,8 +264,8 @@ class TimelineRepositoryTest { dbResult.status = dbStatus.toEntity(account.id, gson) dbResult.account = status.account.toEntity(account.id, gson) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success((listOf(status))))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) @@ -281,8 +282,8 @@ class TimelineRepositoryTest { val dbResult2 = TimelineStatusWithAccount() dbResult2.status = Placeholder("1").toEntity(account.id) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success(listOf(status)))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult, dbResult2))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) diff --git a/fastlane/metadata/android/bg/changelogs/61.txt b/fastlane/metadata/android/bg/changelogs/61.txt new file mode 100644 index 000000000..c6fed8113 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Поддръжка за показване на анкети, гласуване и известия за анкети +- Нови бутони за филтриране на раздела за известия и за изтриване на всички известия +- изтриване и преработване на вашите собствени публикации +- нов индикатор, който показва дали даден акаунт е бот на изображението на профила (може да бъде изключен в предпочитанията) +- Нови преводи: норвежки, букмал и словенски. diff --git a/fastlane/metadata/android/bg/changelogs/67.txt b/fastlane/metadata/android/bg/changelogs/67.txt new file mode 100644 index 000000000..9cf27a894 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Вече можете да създавате анкети от Tusky +- Подобрено търсене +- Нова опция в Предпочитания на акаунта за винаги разширяване на предупрежденията за съдържание +- Аватарите в навигационното чекмедже вече имат закръглена квадратна форма +- Вече е възможно да докладвате за потребители, дори когато те никога не са публикували статус +- Tusky сега ще откаже да се свързва чрез връзки с чист текст на Android 6+ +- Много други малки подобрения и корекции на грешки diff --git a/fastlane/metadata/android/bg/changelogs/68.txt b/fastlane/metadata/android/bg/changelogs/68.txt new file mode 100644 index 000000000..ae09bdf13 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Тази версия осигурява съвместимост с Mastodon 3 и подобрява производителността и стабилността. diff --git a/fastlane/metadata/android/bg/changelogs/70.txt b/fastlane/metadata/android/bg/changelogs/70.txt new file mode 100644 index 000000000..d1cca33af --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Вече можете да маркирате състояния и да показвате отметките си в Tusky. +- Вече можете да планирате публикации с Tusky. Имайте предвид, че избраното време трябва да бъде поне 5 минути в бъдеще. +- Вече можете да добавяте списъци към главния екран. +- Вече можете да публикувате аудио прикачени файлове с Tusky. + +И много други малки подобрения и корекции на грешки! diff --git a/fastlane/metadata/android/bg/changelogs/74.txt b/fastlane/metadata/android/bg/changelogs/74.txt new file mode 100644 index 000000000..4bcdadaf7 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Подобрен основен интерфейс - вече можете да премествате разделите отдолу +- Когато заглушавате потребител, вече можете също да решите дали да заглушите известията му +- Вече можете да следвате колкото искате хештегове в един единствен раздел хештегове +- Подобрен е начинът, по който се показват описанията на мултимедиите, така че да работи дори за супер дълги описания + +Пълен дневник на промените: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/bg/changelogs/77.txt b/fastlane/metadata/android/bg/changelogs/77.txt new file mode 100644 index 000000000..e24a0601f --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- поддръжка за бележки в профила (функция на Mastodon 3.2.0) +- поддръжка за администраторски съобщения (функция на Mastodon 3.1.0) + +- аватарът на избрания от вас акаунт вече ще се показва в главната лента с инструменти +- щракването върху показваното име в емисия ще отвори страницата с профила на този потребител + +- много корекции на грешки и малки подобрения +- подобрени преводи diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt new file mode 100644 index 000000000..73ce4354b --- /dev/null +++ b/fastlane/metadata/android/bg/full_description.txt @@ -0,0 +1,12 @@ +Tusky е лек клиент за Mastodon, свободен сървър за социални мрежи с отворен код. + +• Материален дизайн +• Повечето приложени API на Mastodon +• Поддръжка на няколко акаунта +• Тъмна и светла тема с възможност за автоматично превключване в зависимост от часа +• Чернови - съставете публикации и ги запазете за по-късно +• Изберете между различни стилове емоджита +• Оптимизиран за всички размери на екрана +• Напълно отворен код - няма несвободни зависимости като услугите на Google + +За да научите повече за Mastodon, посетете https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bg/short_description.txt b/fastlane/metadata/android/bg/short_description.txt new file mode 100644 index 000000000..d0331150b --- /dev/null +++ b/fastlane/metadata/android/bg/short_description.txt @@ -0,0 +1 @@ +Клиент с няколко акаунта за социалната мрежа Mastodon diff --git a/fastlane/metadata/android/bg/title.txt b/fastlane/metadata/android/bg/title.txt new file mode 100644 index 000000000..0238ffc0a --- /dev/null +++ b/fastlane/metadata/android/bg/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ckb/changelogs/77.txt b/fastlane/metadata/android/ckb/changelogs/77.txt new file mode 100644 index 000000000..ee35b258f --- /dev/null +++ b/fastlane/metadata/android/ckb/changelogs/77.txt @@ -0,0 +1,10 @@ +تاسکی وشانی ١٣.٠ + +- پشتگیری بۆ تێبینیەکانی پرۆفایل (تایبەتمەندی ماستۆدۆن ٣.٢.٠) +- پشتگیری لە راگەیاندنی بەڕێوەبەر (تایبەتمەندی ماستۆدۆن ٣.١.٠) + +- ئێستا ئەژمێری هەڵبژێردراوی هەژمارەکەت لە شریتی ئامڕازی سەرەکی دا پیشان دەدرێت +- کرتە کردن لەسەر ناوی پیشاندان لە هێڵی کات ئێستا لاپەڕەی پرۆفایلی ئەو بەکارهێنەرە هەڵدەدات + +- زۆر چاککردنەوەی هەڵەکان و چاککردنەوەی بچووک +- وەرگێڕانە باشەکان diff --git a/fastlane/metadata/android/ckb/full_description.txt b/fastlane/metadata/android/ckb/full_description.txt new file mode 100644 index 000000000..ab90d927a --- /dev/null +++ b/fastlane/metadata/android/ckb/full_description.txt @@ -0,0 +1,12 @@ +توسکی ئەپێکی سووکەڵە بۆ ماستۆدۆنە، خزمەتکاری تۆڕی کۆمەڵایەتی ئازاد و کراوە + +• دیزاینی ماتریالی +• زۆربەی ماستۆدۆن API جێبەجێ دەکا +• پشتیوانی هەژمارەی هەمەجۆر +• ڕووکاری تاریک و رووناک لەگەڵ ئەگەری گۆڕینی خۆکار لەسەر بنەمای کاتی رۆژ +• ڕەشنووسەکان - دروستکردنی دووتەکان و هەڵگرتنیان بۆ دواتر +• هەڵبژێرە لەنێوان شێوازە ئیمۆجییە جیاوازەکان +• باشترکراوە بۆ هەموو قەبارەی شاشە +• بەتەواوی کراوەی سەرچاوە - هیچ پشت پێبەستنێکی نائازاد وەک خزمەتگوزاریەکانی گووگڵ + +بۆ زیاتر فێربوون لەبارەی مەستوورن ، سەردانی https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ckb/short_description.txt b/fastlane/metadata/android/ckb/short_description.txt new file mode 100644 index 000000000..df2f8d34f --- /dev/null +++ b/fastlane/metadata/android/ckb/short_description.txt @@ -0,0 +1 @@ +کڕیارێکی هەژماری هەمەجۆر بۆ تۆڕی کۆمەڵایەتی ماستۆدۆن diff --git a/fastlane/metadata/android/ckb/title.txt b/fastlane/metadata/android/ckb/title.txt new file mode 100644 index 000000000..57a4e8902 --- /dev/null +++ b/fastlane/metadata/android/ckb/title.txt @@ -0,0 +1 @@ +تاسکی diff --git a/fastlane/metadata/android/en-US/changelogs/80.txt b/fastlane/metadata/android/en-US/changelogs/80.txt new file mode 100644 index 000000000..14d28f0a9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Get notified when a followed user posts - click the bell icon on their profile! (Mastodon 3.3.0 feature) +- The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy. +- A new wellbeing mode that allows you to limit certain Tusky features has been added. +- Tusky can now animate custom emojis. +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/74.txt b/fastlane/metadata/android/fr/changelogs/74.txt new file mode 100644 index 000000000..f61f152a7 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Amélioration de l'interface principale - vous pouvez maintenant déplacer les onglets vers le bas +- Lorsque vous mettez un utilisateur en sourdine, vous pouvez désormais décider de désactiver ses notifications +- Vous pouvez maintenant suivre autant de hashtags que vous le souhaitez dans un seul onglet hashtag +- La description des médias s'affiche correctement quelque soit sa taille + +Historique complet : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/77.txt b/fastlane/metadata/android/fr/changelogs/77.txt new file mode 100644 index 000000000..1558ac223 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- prise en charge des notes de profil (fonctionnalité de Mastodon 3.2.0) +- le support des annonces de l'administration (fonctionnalité de Mastodon 3.1.0) + +- l'avatar de votre compte sélectionné apparaîtra désormais dans la barre d'outils principale +- en cliquant sur le nom affiché dans une timeline, la page de profil de cet utilisateur s'ouvrira + +- plein corrections de bugs et de petites améliorations +- l'amélioration des traductions diff --git a/fastlane/metadata/android/hu/changelogs/80.txt b/fastlane/metadata/android/hu/changelogs/80.txt new file mode 100644 index 000000000..dd5d58042 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Értesítést kaphatsz, amikor egy követett felhasználó tülköl - csak kattints a csengő ikonra a profilján! (Mastodon 3.3.0 funkció) +- A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb, hibamentesebb legyen. +- Az új jóllét üzemmód lehetővé teszi, hogy bizonyos Tusky funkciókat korlátozz. +- A Tusky mostantól képes animálni az egyedi emojikat is. +Összes változás: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nb_NO/changelogs/80.txt b/fastlane/metadata/android/nb_NO/changelogs/80.txt new file mode 100644 index 000000000..8e4b85957 --- /dev/null +++ b/fastlane/metadata/android/nb_NO/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Mulighet for å bli varslet dersom en bruker du følger publiserer en ny toot - trykk på bjelle-ikonet på profilen deres (krever Mastodon 3.3.0) +- Ny og forbedret kladd-funksjonalitet. +- Velværemodus: Kan brukes til å begrense utvalgt funksjonalitet i Tusky. Du kan aktivere velværemodus i innstillinger. +- Støtte for animerte emojis. Dette er skrudd av som standard, men du kan skru det på i innstillinger. +- Komplett endringelogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/vi/changelogs/80.txt b/fastlane/metadata/android/vi/changelogs/80.txt new file mode 100644 index 000000000..3d54a1a97 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/80.txt @@ -0,0 +1,9 @@ +Tusky v14.0 + +- Thông báo khi người bạn theo dõi đăng tút - click vào biểu tượng cái chuông trên trang cá nhân của họ! (Mastodon 3.3.0) +- Tút Nháp: được thiết kế lại toàn bộ, giúp nhanh hơn, dễ dùng hơn và ít lỗi hơn. +- Chế độ Cai Nghiện: cho phép bạn giới hạn một số tính năng của Tusky. +- Hỗ trợ Emoji động: cho phép xem emoji động trong Tusky. +- Ẩn Có Thời Hạn: có thể chặn người nào đó trong khoảng thời gian cho trước. +- Sửa các lỗi vặt, đặc biệt là sự tương thích Pleroma. +- Cải thiện bản dịch diff --git a/gradle.properties b/gradle.properties index ff715c0e9..dab205f58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,8 @@ org.gradle.jvmargs=-Xmx4096m # use parallel execution org.gradle.parallel=true +# enable file system watching +org.gradle.vfs.watch=true android.enableR8.fullMode=true android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c0535..e708b1c02 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 33682bbbf..1c4bcc29e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c5158..4f906e0c8 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index a9f778a7a..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell