From ee69879c0b69a8251e68d62d71bddbe5acd70961 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 4 Mar 2021 19:36:39 +0100 Subject: [PATCH 01/41] rename fastlane language directories to use bcp47 format closes #2103 --- fastlane/metadata/android/{bn_IN => bn-IN}/changelogs/67.txt | 0 fastlane/metadata/android/{bn_IN => bn-IN}/full_description.txt | 0 fastlane/metadata/android/{bn_IN => bn-IN}/short_description.txt | 0 fastlane/metadata/android/{bn_IN => bn-IN}/title.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/58.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/61.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/67.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/68.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/70.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/72.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/74.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/77.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/80.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/full_description.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/short_description.txt | 0 fastlane/metadata/android/{nb_NO => nb-NO}/title.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/58.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/61.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/67.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/68.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/70.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/72.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/74.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/full_description.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/short_description.txt | 0 fastlane/metadata/android/{pt_BR => pt-BR}/title.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/58.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/61.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/67.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/68.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/70.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/72.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/74.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/77.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/80.txt | 0 .../metadata/android/{zh_Hans => zh-Hans}/full_description.txt | 0 .../metadata/android/{zh_Hans => zh-Hans}/short_description.txt | 0 fastlane/metadata/android/{zh_Hans => zh-Hans}/title.txt | 0 fastlane/metadata/android/{zh_Hant => zh-Hant}/title.txt | 0 39 files changed, 0 insertions(+), 0 deletions(-) rename fastlane/metadata/android/{bn_IN => bn-IN}/changelogs/67.txt (100%) rename fastlane/metadata/android/{bn_IN => bn-IN}/full_description.txt (100%) rename fastlane/metadata/android/{bn_IN => bn-IN}/short_description.txt (100%) rename fastlane/metadata/android/{bn_IN => bn-IN}/title.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/58.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/61.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/67.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/68.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/70.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/72.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/74.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/77.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/changelogs/80.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/full_description.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/short_description.txt (100%) rename fastlane/metadata/android/{nb_NO => nb-NO}/title.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/58.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/61.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/67.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/68.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/70.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/72.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/changelogs/74.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/full_description.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/short_description.txt (100%) rename fastlane/metadata/android/{pt_BR => pt-BR}/title.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/58.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/61.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/67.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/68.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/70.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/72.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/74.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/77.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/changelogs/80.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/full_description.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/short_description.txt (100%) rename fastlane/metadata/android/{zh_Hans => zh-Hans}/title.txt (100%) rename fastlane/metadata/android/{zh_Hant => zh-Hant}/title.txt (100%) diff --git a/fastlane/metadata/android/bn_IN/changelogs/67.txt b/fastlane/metadata/android/bn-IN/changelogs/67.txt similarity index 100% rename from fastlane/metadata/android/bn_IN/changelogs/67.txt rename to fastlane/metadata/android/bn-IN/changelogs/67.txt diff --git a/fastlane/metadata/android/bn_IN/full_description.txt b/fastlane/metadata/android/bn-IN/full_description.txt similarity index 100% rename from fastlane/metadata/android/bn_IN/full_description.txt rename to fastlane/metadata/android/bn-IN/full_description.txt diff --git a/fastlane/metadata/android/bn_IN/short_description.txt b/fastlane/metadata/android/bn-IN/short_description.txt similarity index 100% rename from fastlane/metadata/android/bn_IN/short_description.txt rename to fastlane/metadata/android/bn-IN/short_description.txt diff --git a/fastlane/metadata/android/bn_IN/title.txt b/fastlane/metadata/android/bn-IN/title.txt similarity index 100% rename from fastlane/metadata/android/bn_IN/title.txt rename to fastlane/metadata/android/bn-IN/title.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/58.txt b/fastlane/metadata/android/nb-NO/changelogs/58.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/58.txt rename to fastlane/metadata/android/nb-NO/changelogs/58.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/61.txt b/fastlane/metadata/android/nb-NO/changelogs/61.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/61.txt rename to fastlane/metadata/android/nb-NO/changelogs/61.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/67.txt b/fastlane/metadata/android/nb-NO/changelogs/67.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/67.txt rename to fastlane/metadata/android/nb-NO/changelogs/67.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/68.txt b/fastlane/metadata/android/nb-NO/changelogs/68.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/68.txt rename to fastlane/metadata/android/nb-NO/changelogs/68.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/70.txt b/fastlane/metadata/android/nb-NO/changelogs/70.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/70.txt rename to fastlane/metadata/android/nb-NO/changelogs/70.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/72.txt b/fastlane/metadata/android/nb-NO/changelogs/72.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/72.txt rename to fastlane/metadata/android/nb-NO/changelogs/72.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/74.txt b/fastlane/metadata/android/nb-NO/changelogs/74.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/74.txt rename to fastlane/metadata/android/nb-NO/changelogs/74.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/77.txt b/fastlane/metadata/android/nb-NO/changelogs/77.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/77.txt rename to fastlane/metadata/android/nb-NO/changelogs/77.txt diff --git a/fastlane/metadata/android/nb_NO/changelogs/80.txt b/fastlane/metadata/android/nb-NO/changelogs/80.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/changelogs/80.txt rename to fastlane/metadata/android/nb-NO/changelogs/80.txt diff --git a/fastlane/metadata/android/nb_NO/full_description.txt b/fastlane/metadata/android/nb-NO/full_description.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/full_description.txt rename to fastlane/metadata/android/nb-NO/full_description.txt diff --git a/fastlane/metadata/android/nb_NO/short_description.txt b/fastlane/metadata/android/nb-NO/short_description.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/short_description.txt rename to fastlane/metadata/android/nb-NO/short_description.txt diff --git a/fastlane/metadata/android/nb_NO/title.txt b/fastlane/metadata/android/nb-NO/title.txt similarity index 100% rename from fastlane/metadata/android/nb_NO/title.txt rename to fastlane/metadata/android/nb-NO/title.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/58.txt b/fastlane/metadata/android/pt-BR/changelogs/58.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/58.txt rename to fastlane/metadata/android/pt-BR/changelogs/58.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/61.txt b/fastlane/metadata/android/pt-BR/changelogs/61.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/61.txt rename to fastlane/metadata/android/pt-BR/changelogs/61.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/67.txt b/fastlane/metadata/android/pt-BR/changelogs/67.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/67.txt rename to fastlane/metadata/android/pt-BR/changelogs/67.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/68.txt b/fastlane/metadata/android/pt-BR/changelogs/68.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/68.txt rename to fastlane/metadata/android/pt-BR/changelogs/68.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/70.txt b/fastlane/metadata/android/pt-BR/changelogs/70.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/70.txt rename to fastlane/metadata/android/pt-BR/changelogs/70.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/72.txt b/fastlane/metadata/android/pt-BR/changelogs/72.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/72.txt rename to fastlane/metadata/android/pt-BR/changelogs/72.txt diff --git a/fastlane/metadata/android/pt_BR/changelogs/74.txt b/fastlane/metadata/android/pt-BR/changelogs/74.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/changelogs/74.txt rename to fastlane/metadata/android/pt-BR/changelogs/74.txt diff --git a/fastlane/metadata/android/pt_BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/full_description.txt rename to fastlane/metadata/android/pt-BR/full_description.txt diff --git a/fastlane/metadata/android/pt_BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/short_description.txt rename to fastlane/metadata/android/pt-BR/short_description.txt diff --git a/fastlane/metadata/android/pt_BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt similarity index 100% rename from fastlane/metadata/android/pt_BR/title.txt rename to fastlane/metadata/android/pt-BR/title.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/58.txt b/fastlane/metadata/android/zh-Hans/changelogs/58.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/58.txt rename to fastlane/metadata/android/zh-Hans/changelogs/58.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/61.txt b/fastlane/metadata/android/zh-Hans/changelogs/61.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/61.txt rename to fastlane/metadata/android/zh-Hans/changelogs/61.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/67.txt b/fastlane/metadata/android/zh-Hans/changelogs/67.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/67.txt rename to fastlane/metadata/android/zh-Hans/changelogs/67.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/68.txt b/fastlane/metadata/android/zh-Hans/changelogs/68.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/68.txt rename to fastlane/metadata/android/zh-Hans/changelogs/68.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/70.txt b/fastlane/metadata/android/zh-Hans/changelogs/70.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/70.txt rename to fastlane/metadata/android/zh-Hans/changelogs/70.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/72.txt b/fastlane/metadata/android/zh-Hans/changelogs/72.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/72.txt rename to fastlane/metadata/android/zh-Hans/changelogs/72.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/74.txt b/fastlane/metadata/android/zh-Hans/changelogs/74.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/74.txt rename to fastlane/metadata/android/zh-Hans/changelogs/74.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/77.txt b/fastlane/metadata/android/zh-Hans/changelogs/77.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/77.txt rename to fastlane/metadata/android/zh-Hans/changelogs/77.txt diff --git a/fastlane/metadata/android/zh_Hans/changelogs/80.txt b/fastlane/metadata/android/zh-Hans/changelogs/80.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/changelogs/80.txt rename to fastlane/metadata/android/zh-Hans/changelogs/80.txt diff --git a/fastlane/metadata/android/zh_Hans/full_description.txt b/fastlane/metadata/android/zh-Hans/full_description.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/full_description.txt rename to fastlane/metadata/android/zh-Hans/full_description.txt diff --git a/fastlane/metadata/android/zh_Hans/short_description.txt b/fastlane/metadata/android/zh-Hans/short_description.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/short_description.txt rename to fastlane/metadata/android/zh-Hans/short_description.txt diff --git a/fastlane/metadata/android/zh_Hans/title.txt b/fastlane/metadata/android/zh-Hans/title.txt similarity index 100% rename from fastlane/metadata/android/zh_Hans/title.txt rename to fastlane/metadata/android/zh-Hans/title.txt diff --git a/fastlane/metadata/android/zh_Hant/title.txt b/fastlane/metadata/android/zh-Hant/title.txt similarity index 100% rename from fastlane/metadata/android/zh_Hant/title.txt rename to fastlane/metadata/android/zh-Hant/title.txt From cf642d9eb0e6eae6ffec9e7c8d21927b5bd2fc7a Mon Sep 17 00:00:00 2001 From: kyori19 Date: Sat, 6 Mar 2021 00:26:51 +0900 Subject: [PATCH 02/41] Avoid using displayName for displaying purpose (#2101) --- .../tusky/components/conversation/ConversationEntity.kt | 2 +- .../com/keylesspalace/tusky/repository/TimelineRepository.kt | 2 +- .../main/java/com/keylesspalace/tusky/util/ViewDataUtils.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 8ee1a284a..e35d460d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -167,7 +167,7 @@ fun Account.toEntity() = ConversationAccountEntity( id, username, - displayName.orEmpty(), + name, avatar, emojis ?: emptyList() ) 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 945c55d33..b3e12aeb0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -298,7 +298,7 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { timelineUserId = accountId, localUsername = localUsername, username = username, - displayName = displayName.orEmpty(), + displayName = name, url = url, avatar = avatar, emojis = gson.toJson(emojis), 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 ffe64a14b..2e8e67efc 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().getDisplayName()) + .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getName()) .setUserFullName(visibleStatus.getAccount().getName()) .setVisibility(visibleStatus.getVisibility()) .setSenderId(visibleStatus.getAccount().getId()) From 63c1092951ba4d58ea5077f8338656ab70cce7e3 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 7 Mar 2021 02:37:03 +0000 Subject: [PATCH 03/41] Translated using Weblate (Ukrainian) Currently translated at 100.0% (12 of 12 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ Translated using Weblate (Ukrainian) Currently translated at 33.3% (4 of 12 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/58.txt | 13 +++++++++++++ fastlane/metadata/android/uk/changelogs/61.txt | 7 +++++++ fastlane/metadata/android/uk/changelogs/67.txt | 9 +++++++++ fastlane/metadata/android/uk/changelogs/68.txt | 2 +- fastlane/metadata/android/uk/changelogs/70.txt | 8 ++++++++ fastlane/metadata/android/uk/changelogs/72.txt | 11 +++++++++++ fastlane/metadata/android/uk/changelogs/74.txt | 8 ++++++++ fastlane/metadata/android/uk/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/uk/changelogs/80.txt | 7 +++++++ fastlane/metadata/android/uk/full_description.txt | 14 +++++++------- fastlane/metadata/android/uk/short_description.txt | 2 +- 11 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 fastlane/metadata/android/uk/changelogs/58.txt create mode 100644 fastlane/metadata/android/uk/changelogs/61.txt create mode 100644 fastlane/metadata/android/uk/changelogs/67.txt create mode 100644 fastlane/metadata/android/uk/changelogs/70.txt create mode 100644 fastlane/metadata/android/uk/changelogs/72.txt create mode 100644 fastlane/metadata/android/uk/changelogs/74.txt create mode 100644 fastlane/metadata/android/uk/changelogs/77.txt create mode 100644 fastlane/metadata/android/uk/changelogs/80.txt diff --git a/fastlane/metadata/android/uk/changelogs/58.txt b/fastlane/metadata/android/uk/changelogs/58.txt new file mode 100644 index 000000000..bdfc65175 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Фільтри стрічки в Налаштуваннях облікового запису та їхня синхронізація з сервером +- Можна мати власний хештег вкладкою основного інтерфейсу +- Списки можна редагувати +- Безпека: вилучено підтримку TLS 1.0 та TLS 1.1 та додано підтримку TLS 1.3 на Android 6+ +- Пропонування власних смайлів, під час введення +- «Слідувати системній темі» +- Поліпшено доступність стрічки +- Нехтування невідомими сповіщеннями без збоїв +- Можна змінювати мову +- Нові переклади: чеська та есперанто +- Інше diff --git a/fastlane/metadata/android/uk/changelogs/61.txt b/fastlane/metadata/android/uk/changelogs/61.txt new file mode 100644 index 000000000..3c184a05e --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Підтримка показу опитувань, голосування та повідомлень про опитування +- Нові кнопки фільтрування вкладки сповіщень та видалення всіх сповіщень +- видалити та переробити власні дмухи +- новий індикатор, який показує, чи є обліковий запис ботом на зображенні профілю (можна вимкнути в налаштуваннях) +- Нові переклади: норвезька букмол та словенська. diff --git a/fastlane/metadata/android/uk/changelogs/67.txt b/fastlane/metadata/android/uk/changelogs/67.txt new file mode 100644 index 000000000..6c3eb9794 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Тепер ви можете створювати опитування з Tusky +- Вдосконалено пошук +- Новий параметр налаштувань облікового запису — завжди розгортати попередження щодо вмісту +- Аватари в навігаційній панелі тепер мають округлу квадратну форму +- Тепер можна повідомляти про користувачів, навіть якщо вони ніколи не розміщували статус +- Tusky тепер відмовиться під'єднуватись через незахищене з’єднання на Android 6+ +- Багато інших невеликих удосконалень та виправлень diff --git a/fastlane/metadata/android/uk/changelogs/68.txt b/fastlane/metadata/android/uk/changelogs/68.txt index 449a43985..e649dd6e5 100644 --- a/fastlane/metadata/android/uk/changelogs/68.txt +++ b/fastlane/metadata/android/uk/changelogs/68.txt @@ -1,3 +1,3 @@ Tusky v9.1 -Цей реліз забезпечує сумісність з Mastodon 3, підвищує продуктивність і стабільність. +Цей випуск забезпечує сумісність з Mastodon 3, підвищує продуктивність і стабільність. diff --git a/fastlane/metadata/android/uk/changelogs/70.txt b/fastlane/metadata/android/uk/changelogs/70.txt new file mode 100644 index 000000000..cdec5d1e8 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Тепер ви можете закріпити статус і перелічити свої закладки в Tusky. +- Тепер ви можете запланувати дмухи з Tusky. Зверніть увагу, що час, який ви вибираєте, має бути не раніше 5 хвилин у майбутньому. +- Тепер ви можете додати списки на головний екран. +- Тепер ви можете розмістити аудіовкладення з Tusky. + +І багато інших невеликих поліпшень та виправлень помилок! diff --git a/fastlane/metadata/android/uk/changelogs/72.txt b/fastlane/metadata/android/uk/changelogs/72.txt new file mode 100644 index 000000000..e8d9e8211 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Сповіщення про нові запити, коли ваш обліковий запис заблоковано +- Нові функції, які можна перемикати на екрані налаштувань: + - вимкнути перекидання між вкладками + - показ діалогового вікна підтвердження, перед дмухом + - показ попереднього перегляду посилання в стрічці +- Розмови тепер можна приглушити +- Вдосконалено голосування +- Виправлено помилки, більшість з яких пов'язані зі створенням дмухів +- Поліпшено переклади diff --git a/fastlane/metadata/android/uk/changelogs/74.txt b/fastlane/metadata/android/uk/changelogs/74.txt new file mode 100644 index 000000000..6b039de9e --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Вдосконалено основний інтерфейс — тепер ви можете переміщувати вкладки вниз +- Заглушивши користувача, ви також можете вирішити, чи ігнорувати його сповіщення +- Тепер ви можете відстежувати скільки завгодно хештегів на окремій вкладці хештегів +- Поліпшено спосіб показу описів медіа, тому він працює навіть для наддовгих описів + +Журнал усіх змін: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/uk/changelogs/77.txt b/fastlane/metadata/android/uk/changelogs/77.txt new file mode 100644 index 000000000..fbbda2db0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- підтримка приміток профілю (функція Mastodon 3.2.0) +- підтримка повідомлень адміністратора (функція Mastodon 3.1.0) + +- аватар вибраного облікового запису тепер з'явиться на головній панелі інструментів +- натискання показуваного імені в стрічці відкриє сторінку профілю цього користувача + +- багато виправлень та невеликих удосконалень +- вдосконалено переклади diff --git a/fastlane/metadata/android/uk/changelogs/80.txt b/fastlane/metadata/android/uk/changelogs/80.txt new file mode 100644 index 000000000..e4cccad43 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Отримуйте сповіщення, коли користувач, який стежить за дописами, клацне піктограму дзвоника в його профілі! (Функція Mastodon 3.3.0) +- Функція чернетки в Tusky була повністю перероблена, щоб стати швидшою, зручнішою для користувачів і з меншою кількістю помилок. +- Додано новий режим добробуту, який дозволяє обмежити певні функції Tusky. +- Tusky тепер може анімувати власні смайли. +Повний журнал змін: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 8e171ad06..2a76e2351 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -1,12 +1,12 @@ -Tusky — це легкий клієнт для Mastodon, безкоштовної соціальної мережі з відкритим кодом сервера. +Tusky — це легкий клієнт для Mastodon, безплатної соціальної мережі з відкритим кодом сервера. • Дизайн в стилі Material • Підтримка більшості можливостей Mastodon -• Підтримка безлічі акаунтів одночасно -• Темна і світла теми з можливістю автоматичного переключення в залежності від часу доби -• Чернетки: почніть створювати пост і збережіть його на потім +• Підтримка кількох облікових записів +• Темна та світла теми з можливістю автоперемикання залежно від часу доби +• Чернетки — почніть створювати допис і збережіть його на потім • Вибір між різними наборами емодзі -• Додаток оптимізовано під різні розміри екрану -• Відкритий вихідний код, ніяких пропрієтарних компонентів, начебто сервісів Google. +• Застосунок оптимізовано для різних розмірів екрана +• Відкритий джерельний код, ніяких не вільних складників, як-от служб Google. -Щоб дізнатися більше про Mastodon, перейдіть на https://joinmastodon.org/ +Щоб дізнатися більше про Mastodon, відвідайте https://joinmastodon.org/ diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt index d29ad27b9..33eeef049 100644 --- a/fastlane/metadata/android/uk/short_description.txt +++ b/fastlane/metadata/android/uk/short_description.txt @@ -1 +1 @@ -Клієнт для соціальної мережі Mastodon з підтримкою декількох акаунтів +Клієнт соціальної мережі Mastodon з підтримкою кількох облікових записів From 47e5f2b95408700a2fa8af1dd1e54479c7ecedbd Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 7 Mar 2021 10:06:25 +0000 Subject: [PATCH 04/41] Translated using Weblate (Ukrainian) Currently translated at 31.3% (144 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5c49c828c..7db993e53 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -139,12 +139,15 @@ Приховані домени Заглушені користувачі Дописи - Поширити + Дмухнути Вкладки Завантаження не вдалося. - Не вдалося отримати токін авторизації. + Не вдалося отримати токен входу. Авторизація була відхилина. - Сталася помилка неопізнаної авторизації. - Помилка входу з цією інстанцією. + Сталася помилка невпізнання авторизації. + Помилка автентифікації цього сервера. Введено недійсний домен + Показати просування + Показати просування + Вкладки \ No newline at end of file From b92f26c71af04beb09ee1edd06a60ed6dbf881f3 Mon Sep 17 00:00:00 2001 From: idontwanttohaveausername Date: Sun, 7 Mar 2021 10:06:26 +0000 Subject: [PATCH 05/41] Translated using Weblate (Ukrainian) Currently translated at 31.5% (145 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 31.5% (145 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 31.3% (144 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7db993e53..05e8c1b84 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -109,7 +109,7 @@ Змінити Написати Скасувати приглушення бесіди - Приглушити бесіду + Заглушити діалог Заплановані пости Підписники Написати @@ -134,14 +134,14 @@ %s просунув(ла) ваш статус Згорнути Розгорнути - Чутливий вміст + Дражливий зміст %s Просунув(ла) Приховані домени Заглушені користувачі Дописи Дмухнути Вкладки - Завантаження не вдалося. + Не вдалося завантажити файл. Не вдалося отримати токен входу. Авторизація була відхилина. Сталася помилка невпізнання авторизації. From d2a6b444f630d5841f6e028831969eb87482b309 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 7 Mar 2021 10:06:26 +0000 Subject: [PATCH 06/41] Translated using Weblate (Ukrainian) Currently translated at 35.7% (164 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 34.4% (158 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 31.8% (146 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 31.5% (145 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 31.5% (145 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 90 ++++++++++++++++---------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 05e8c1b84..d507f1cbb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,6 +1,6 @@ - Сталася неочікувана помилка. + Сталася помилка. \@%s Ліцензії Редагувати профіль @@ -10,36 +10,36 @@ Підписки Прикріплені З відповідями - Глобальна - Локальна + Загальні + Локальні Приватні повідомлення Сповіщення Головна - Помилка при надісланні поста. + Помилка надіслання допису. Зображення та відео не можуть бути прикріплені до статусу одночасно. Потрібен дозвіл на зберігання медіа. Потрібен дозвіл на читання медіа. - Файл не вдається відкрити. - Файл такого типу неможливо завантажити. + Не вдається відкрити цей файл. + Неможливо відвантажити файл цього типу. Аудіофайли повинні бути менше 40 МБ. Відео повинне бути менше 40 МБ. Файл повинен бути менше 8 МБ. - Статус занадто довгий! - Не вдалося знайти веб-браузер, який можна використати. + Статус надто довгий! + Не вдалося знайти браузер, який можна використати. Не може бути порожнім. - Сталася помилка мережі! Будь ласка, перевірте інтернет-з\'єднання і спробуйте знову! + Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову! Списки Списки - Про додаток + Про застосунок Скинути Пошук Редагувати профіль - Налаштування акаунта + Налаштування облікового запису Налаштування Вийти Чернетки Вподобане - Увійти + Увійти з Mastodon Зʼєднання… Немає результатів Пошук… @@ -56,8 +56,8 @@ Показати, хто вподобав Згадки Посилання - Попередження про контент - Заплановані пости + Попередження про вміст + Заплановані дописи Чернетки Відмовити Прийняти @@ -92,25 +92,25 @@ Написати Не подобається Додати в закладки - Подобається + Вподобати Відповісти Швидка відповідь Додаткові коментарі\? Поскаржитися на @%s - %s відправив(-ла) запит на підписку - %s підписався(-лась) на вас + %s надсилає запит на підписку + %s підписується на вас Тут нічого немає. Потягніть вниз, щоб оновити! Тут нічого немає. Згорнути Розгорнути Натисніть для перегляду Медіа приховано - Попередження про контент + Попередження про вміст Змінити Написати - Скасувати приглушення бесіди - Заглушити діалог - Заплановані пости + Скасувати приглушення розмови + Заглушити розмову + Заплановані дмухи Підписники Написати Медіа @@ -119,35 +119,59 @@ Заблоковані користувачі Вподобане Запити на підписку - Розблокувати %s - Відмінити приглушення + Не глушити %s + Не глушити Приховані домени - Список глушіння - ТООТ! - ТООТ + Заглушені користувачі + ДМУХНУТИ! + ДМУХНУТИ Показати просування Приховати просування Розгорнути - Забртаи просунення + Прибрати просування Просунути - %s сподабався ваш статус - %s просунув(ла) ваш статус + %s вподобує ваш дмух + %s просуває ваш дмух Згорнути Розгорнути - Дражливий зміст - %s Просунув(ла) + Делікатний вміст + %s просуває Приховані домени Заглушені користувачі Дописи Дмухнути Вкладки - Не вдалося завантажити файл. + Не вдалося відвантажити. Не вдалося отримати токен входу. - Авторизація була відхилина. + Авторизацію відхилено. Сталася помилка невпізнання авторизації. Помилка автентифікації цього сервера. Введено недійсний домен Показати просування Показати просування Вкладки + Не глушити %s + Видимість дмухів + Деякі відомості, які можуть вплинути на ваше психічний стан, буде приховано. Це включає: +\n +\n - Вподобання/Просування/Сповіщення про підписки +\n - Вподобання/Кількість просувань дмухів +\n - Статистика підписників/Публікацій у профілях +\n +\n На push-сповіщення це не вплине, але ви можете переглянути налаштування сповіщень вручну. + Вподобано + Вподобали + + %1$s вподобання + %1$s вподобання + %1$s вподобань + %1$s вподобань + + Сповіщати про вподобання кимось дмухів + мої дописи вподобано + Сховати медіа + Заглушити сповіщення від %s + Не глушити сповіщення від %s + %s щойно опубліковано + Оголошення \ No newline at end of file From 6d86fdfc82fdf3c2db1b0e552c46e30e346025f3 Mon Sep 17 00:00:00 2001 From: idontwanttohaveausername Date: Sun, 7 Mar 2021 10:06:26 +0000 Subject: [PATCH 07/41] Translated using Weblate (Ukrainian) Currently translated at 35.7% (164 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 34.4% (158 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d507f1cbb..43cf2fa56 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -57,9 +57,9 @@ Згадки Посилання Попередження про вміст - Заплановані дописи + Заплановані дмухи Чернетки - Відмовити + Відхилити Прийняти Скасувати Змінити @@ -174,4 +174,5 @@ Не глушити сповіщення від %s %s щойно опубліковано Оголошення + Намалювати \ No newline at end of file From d84c07eb825ffb20e1e4c6f7460fe30a8ddaa927 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 7 Mar 2021 10:06:26 +0000 Subject: [PATCH 08/41] Translated using Weblate (Ukrainian) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translated using Weblate (Ukrainian) Currently translated at 86.7% (398 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 342 ++++++++++++++++++++++++- 1 file changed, 341 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 43cf2fa56..5b0c14c8c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -174,5 +174,345 @@ Не глушити сповіщення від %s %s щойно опубліковано Оголошення - Намалювати + Відкрити меню + Емодзі Blob з Android 4.4–7.1 + Типовий набір емодзі пристрою + Виконання пошуку… + Спочатку потрібно буде завантажити ці набори емодзі + Типовий системний + Стиль емодзі + Скопійовано до буфера обміну + Ваш сервер %s не має власних емодзі + Зберегти чернетку\? + Вимагає затвердження підписників власноруч + Додати підпис + Опис для людей з порушеннями зору +\n(до %d символів) + Не вдалося додати підпис + Відписатися + Підписатися + Збережено! + Вибір %d + Кілька виборів + Додати вибір + 7 днів + 3 дні + 1 день + 6 годин + 1 година + 30 хвилин + 5 хвилин + Безкінечно + Тривалість + Опитування + Облікові записи + Не вдалося звітувати + Додаткові коментарі + Звіт @%s надіслано + Готово + Назад + Продовжити + + Залишилася %d секунда + Залишилося %d секунди + Залишилося %d секунд + Залишилося %d секунд + + + Залишилася %d хвилина + Залишилося %d хвилини + Залишилося %d хвилин + Залишилося %d хвилин + + + Залишилася %d година + Залишилося %d години + Залишилося %d годин + Залишилося %d годин + + + Залишився %d день + Залишилося %d дні + Залишилося %d днів + Залишилося %d днів + + Створене вами опитування завершилося + Опитування, в якому ви проголосували + Голосувати + закрито + завершується о %s + + %s особа + %s особи + %s осіб + %s осіб + + + %s голос + %s голоси + %s голосів + %s голосів + + %1$s • %2$s + Без опису + Попередження про вміст: %s + Медіа: %s + Просунули + Вміст + CC-BY-SA 4.0 + CC-BY 4.0 + Бот + Перезапустити + Пізніше + Вам потрібно буде перезапустити Tusky, щоб застосувати ці зміни + Необхідно перезапустити застосунок + Відкрити дмух + Розгорнути/згорнути всі статуси + Копію дмуху збережено до ваших чернеток + Надсилання скасовано + Надсилання дмухів + Помилка надсилання дмуху + Надсилання дмуху… + Оприлюднення з облікового запису %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 та ще %3$d + %1$s та %2$s + %1$s + %1$s та %2$s + %1$s, %2$s, та %3$s + %1$s, %2$s, %3$s та %4$d інших + %s згадує вас + Сповіщати про нові дмухи осіб, на яких ви підписалися + Нові дмухи + Сповіщати про завершення опитувань + Опитування + Сповіщати про просування кимось + Просування + Сповіщати про нові запити на підписки + Сповіщати про нових підписників + Нові підписники + Сповіщати про нові згадки + Нові згадки + Найбільший + Великий + Середній + Маленький + Найменший + Розмір шрифту статусу + Лише для підписників + Приховано + Приховано + Публічно + Публічно + Внизу + Вгорі + Розташування головної панелі переходів + Не вдалося синхронізувати налаштування + Публікування (синхронізовано з сервером) + Завжди позначати дописи делікатними + Типова приватність дописів + Порт HTTP-проксі + Сервер HTTP-проксі + Увімкнути HTTP-проксі + HTTP-проксі + Проксі + Завантаження попереднього перегляду медіа + Показати відповіді + Фільтрування стрічки + Анімувати власні емодзі + Показувати барвисті градієнти замість прихованих медіа + Анімовані GIF-аватарки + Показувати позначки для ботів + Мова + Ховати кнопку написати під час прокручування + Вкладки вбудованого браузера Chrome + Браузер + Тема системи + Автоматична від заходу сонця + Чорна + Світла + Темна + Фільтри + Стрічки + Тема застосунку + Вигляд + хтось, на кого мене підписано, публікує новий дмух + опитування завершено + мої дописи просунуто + отримано запит на підписку + хтось підписується + мене згадано + Сповіщати мене коли + Світлосповіщення + Вібросповіщення + Звукові сповіщення + Попередження + Безпосередньо: Опублікувати лише для згаданих користувачів + Лише підписники: Опублікувати лише для підписників + Приховано: Не показувати у загальних стрічках + Публічно: Опублікувати у загальних стрічках + Сховати сповіщення + Заглушити @%s\? + Заблокувати @%s\? + Сховати весь домен + Ви впевнені, що хочете заблокувати все з %s\? Ви не побачите вміст із цього домену в жодних загальнодоступних стрічках або у своїх сповіщеннях. Ваших підписників з цього домену буде видалено. + Видалити й переписати цей дмух\? + Видалити цей дмух\? + Не стежити за цим обліковим записом\? + Відкликати запит на підписку\? + Завантаження + Відвантаження… + Завершення відвантаження медіа + Сюди можна ввести адресу або домен будь-якого сервера, наприклад mastodon.social, icosahedron.website, social.tchncs.de та більше! +\n +\nЯкщо у вас ще немає облікового запису, ви можете ввести назву сервера, до якого ви хочете приєднатися та створити там обліковий запис. +\n +\nСервер — єдине місце, де розміщено ваш обліковий запис, але ви можете легко спілкуватися з людьми та стежити за ними на інших серверах, ніби ви перебуваєте на тому ж сайті. +\n +\nДокладніше на joinmastodon.org. + Що таке сервер\? + Що таке сервер\? + Заголовок + Аватар + Відповісти… + Показуване ім\'я + Відповідь успішно надіслано. + %s показано + Глушіння користувача прибрано + Користувача розблоковано + Поділитися медіа з… + Поділитися дмухом з… + Поділитися URL-адресою дмуха з… + Завантаження медіа + Завантажити медіа + Відкрити медіа #%d + Хештеги + Хештеги + Хештеги + Відкрити автора просування + Додати вкладку + Запланувати дмух + Клавіотура емодзі + Дмух, для якого ви створили чернетку відповіді, вилучено + Чернетку видалено + Не вдалося завантажити дані відповіді + Старі чернетки + Функція чернетки в Tusky була повністю перероблена, щоб бути швидшою, зручнішою для користувачів і з меншою кількістю вад. +\n Ви все ще можете отримати доступ до своїх старих чернеток за допомогою кнопки на екрані нових чернеток, але вони будуть вилучені в майбутньому оновленні! + Не вдалося надіслати цей дмух! + Ви дійсно хочете видалити список %s\? + Ви не можете завантажити більше %1$d медіавкладень. + Приховати кількісну статистику профілів + Приховати кількісну статистику дописів + Обмеження сповіщень стрічки + Переглянути сповіщення + Ваша особиста примітка щодо цього облікового запису + Добробут + Сховати заголовок верхньої панелі інструментів + Запитувати підтвердження перед просуванням + Показувати попередній перегляд посилань у стрічках + Найкоротший час планування Mastodon становить 5 хвилин. + Оголошень немає. + Черга статусів порожня. + У вас немає чернеток. + Помилка пошуку допису %s + Увімкнути перемикання між вкладками жестом проведення пальцем + Показати фільтр сповіщень + Не вдалося здійснити пошук + Обліковий запис з іншого сервера. Надіслати анонімізовану копію звіту й туди\? + Скаргу буде надіслано вашому модератору сервера. Ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис знизу: + Не вдалося отримати статуси + Переслати до %s + Дії для зображення %s + Ви впевнені, що хочете остаточно очистити всі сповіщення\? + Створити дмух + Застосувати + Фільтр + Очистити + Список + Вибрати список + Хештег без # + Додати хештег + Назва списку + Опитування з варіантами: %1$s, %2$s, %3$s, %4$s; %5$s + Безпосередньо + Додано до закладок + Просунуто + досягнено обмеження %1$d вкладок + + %s просування + %s просування + %s просувань + %s просувань + + Прикріпити + Відкріпити + Наведені далі відомості можуть відбивати не повний профіль користувача. Натисніть, щоб відкрити повний профіль у браузері. + Показ абсолютного часу + Ярлик + додати дані + Метадані профілю + Ліцензовано ліцензією Apache (копія знизу) + Tusky містить код та засоби з таких проєктів з відкритим кодом: + Відкликати просування + Просунути початковій аудиторії + %1$s переміщено до: + Не вдалося завантажити + Поточний набір емодзі Google + Стандартний набір емодзі Mastodon \ No newline at end of file From 5167b8578e135753798a1fa4834a7a8a5b8413e4 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Mar 2021 19:04:22 +0100 Subject: [PATCH 09/41] migrating to ViewBinding part 1: Dialogs + Views (#2091) --- .../compose/dialog/AddPollDialog.kt | 21 +++++++++--------- .../compose/view/PollPreviewView.kt | 16 ++++++-------- .../tusky/view/BackgroundMessageView.kt | 22 +++++++++++-------- .../keylesspalace/tusky/view/LicenseCard.kt | 13 ++++++----- .../tusky/view/MuteAccountDialog.kt | 17 +++++--------- .../main/res/layout/dialog_mute_account.xml | 1 - 6 files changed, 43 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index d0f98bac6..09da54626 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -23,8 +23,8 @@ import android.view.WindowManager import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter +import com.keylesspalace.tusky.databinding.DialogAddPollBinding import com.keylesspalace.tusky.entity.NewPoll -import kotlinx.android.synthetic.main.dialog_add_poll.view.* fun showAddPollDialog( context: Context, @@ -34,12 +34,12 @@ fun showAddPollDialog( onUpdatePoll: (NewPoll) -> Unit ) { - val view = LayoutInflater.from(context).inflate(R.layout.dialog_add_poll, null) + val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context)) val dialog = AlertDialog.Builder(context) .setIcon(R.drawable.ic_poll_24dp) .setTitle(R.string.create_poll_title) - .setView(view) + .setView(binding.root) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, null) .create() @@ -48,7 +48,7 @@ fun showAddPollDialog( options = poll?.options?.toMutableList() ?: mutableListOf("", ""), maxOptionLength = maxOptionLength, onOptionRemoved = { valid -> - view.addChoiceButton.isEnabled = true + binding.addChoiceButton.isEnabled = true dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid }, onOptionChanged = { valid -> @@ -56,9 +56,9 @@ fun showAddPollDialog( } ) - view.pollChoices.adapter = adapter + binding.pollChoices.adapter = adapter - view.addChoiceButton.setOnClickListener { + binding.addChoiceButton.setOnClickListener { if (adapter.itemCount < maxOptionCount) { adapter.addChoice() } @@ -71,14 +71,14 @@ fun showAddPollDialog( it <= poll?.expiresIn ?: 0 } - view.pollDurationSpinner.setSelection(pollDurationId) + binding.pollDurationSpinner.setSelection(pollDurationId) - view.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false + binding.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false dialog.setOnShowListener { val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) button.setOnClickListener { - val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition + val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition val pollDuration = context.resources .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] @@ -86,7 +86,7 @@ fun showAddPollDialog( onUpdatePoll(NewPoll( options = adapter.pollOptions, expiresIn = pollDuration, - multiple = view.multipleChoicesCheckBox.isChecked + multiple = binding.multipleChoicesCheckBox.isChecked )) dialog.dismiss() @@ -97,5 +97,4 @@ fun showAddPollDialog( // make the dialog focusable so the keyboard does not stay behind it dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt index 63e627fc1..1126047d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt @@ -17,11 +17,12 @@ package com.keylesspalace.tusky.components.compose.view import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.widget.LinearLayout import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter +import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding import com.keylesspalace.tusky.entity.NewPoll -import kotlinx.android.synthetic.main.view_poll_preview.view.* class PollPreviewView @JvmOverloads constructor( context: Context?, @@ -29,11 +30,11 @@ class PollPreviewView @JvmOverloads constructor( defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) { - val adapter = PreviewPollOptionsAdapter() + private val adapter = PreviewPollOptionsAdapter() + + private val binding = ViewPollPreviewBinding.inflate(LayoutInflater.from(context), this) init { - inflate(context, R.layout.view_poll_preview, this) - orientation = VERTICAL setBackgroundResource(R.drawable.card_frame) @@ -42,8 +43,7 @@ class PollPreviewView @JvmOverloads constructor( setPadding(padding, padding, padding, padding) - pollPreviewOptions.adapter = adapter - + binding.pollPreviewOptions.adapter = adapter } fun setPoll(poll: NewPoll){ @@ -52,13 +52,11 @@ class PollPreviewView @JvmOverloads constructor( val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { it <= poll.expiresIn } - pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId] - + binding.pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId] } override fun setOnClickListener(l: OnClickListener?) { super.setOnClickListener(l) adapter.setOnClickListener(l) } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index 4789ac3c1..32a7d6b31 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -3,14 +3,14 @@ package com.keylesspalace.tusky.view import android.content.Context import android.util.AttributeSet import android.view.Gravity +import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding import com.keylesspalace.tusky.util.visible -import kotlinx.android.synthetic.main.view_background_message.view.* - /** * This view is used for screens with downloadable content which may fail. @@ -22,8 +22,9 @@ class BackgroundMessageView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { + private val binding = ViewBackgroundMessageBinding.inflate(LayoutInflater.from(context), this) + init { - View.inflate(context, R.layout.view_background_message, this) gravity = Gravity.CENTER_HORIZONTAL orientation = VERTICAL @@ -36,11 +37,14 @@ class BackgroundMessageView @JvmOverloads constructor( * Setup image, message and button. * If [clickListener] is `null` then the button will be hidden. */ - fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, - clickListener: ((v: View) -> Unit)? = null) { - messageTextView.setText(messageRes) - imageView.setImageResource(imageRes) - button.setOnClickListener(clickListener) - button.visible(clickListener != null) + fun setup( + @DrawableRes imageRes: Int, + @StringRes messageRes: Int, + clickListener: ((v: View) -> Unit)? = null + ) { + binding.messageTextView.setText(messageRes) + binding.imageView.setImageResource(imageRes) + binding.button.setOnClickListener(clickListener) + binding.button.visible(clickListener != null) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt index 2c73cd54a..ad9ae52c2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -17,12 +17,13 @@ package com.keylesspalace.tusky.view import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import com.google.android.material.card.MaterialCardView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.CardLicenseBinding import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.card_license.view.* class LicenseCard @JvmOverloads constructor( @@ -32,7 +33,7 @@ class LicenseCard ) : MaterialCardView(context, attrs, defStyleAttr) { init { - inflate(context, R.layout.card_license, this) + val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this) setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) @@ -43,12 +44,12 @@ class LicenseCard val link: String? = a.getString(R.styleable.LicenseCard_link) a.recycle() - licenseCardName.text = name - licenseCardLicense.text = license + binding.licenseCardName.text = name + binding.licenseCardLicense.text = license if(link.isNullOrBlank()) { - licenseCardLink.hide() + binding.licenseCardLink.hide() } else { - licenseCardLink.text = link + binding.licenseCardLink.text = link setOnClickListener { LinkHelper.openLink(link, context) } } 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 435e24501..da5e79874 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -3,29 +3,24 @@ 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 +import com.keylesspalace.tusky.databinding.DialogMuteAccountBinding fun showMuteAccountDialog( activity: Activity, accountUsername: String, 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 = - activity.getString(R.string.dialog_mute_warning, accountUsername) - val checkbox: CheckBox = view.findViewById(R.id.checkbox) - checkbox.isChecked = true + val binding = DialogMuteAccountBinding.inflate(activity.layoutInflater) + binding.warning.text = activity.getString(R.string.dialog_mute_warning, accountUsername) + binding.checkbox.isChecked = true AlertDialog.Builder(activity) - .setView(view) + .setView(binding.root) .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]) + onOk(binding.checkbox.isChecked, durationValues[binding.duration.selectedItemPosition]) } .setNegativeButton(android.R.string.cancel, null) .show() diff --git a/app/src/main/res/layout/dialog_mute_account.xml b/app/src/main/res/layout/dialog_mute_account.xml index b58a277cb..e826445e3 100644 --- a/app/src/main/res/layout/dialog_mute_account.xml +++ b/app/src/main/res/layout/dialog_mute_account.xml @@ -23,7 +23,6 @@ android:text="@string/dialog_mute_hide_notifications"/> Date: Sun, 7 Mar 2021 19:05:51 +0100 Subject: [PATCH 10/41] migrating to ViewBinding part 2: Activities (#2093) --- .../com/keylesspalace/tusky/AboutActivity.kt | 26 +- .../keylesspalace/tusky/AccountActivity.kt | 205 +++++++------- .../tusky/AccountListActivity.kt | 7 +- .../tusky/EditProfileActivity.kt | 84 +++--- .../keylesspalace/tusky/FiltersActivity.kt | 119 ++++---- .../keylesspalace/tusky/LicenseActivity.kt | 15 +- .../com/keylesspalace/tusky/ListsActivity.kt | 58 ++-- .../com/keylesspalace/tusky/LoginActivity.kt | 61 ++-- .../com/keylesspalace/tusky/MainActivity.kt | 84 +++--- .../tusky/ModalTimelineActivity.kt | 45 ++- .../keylesspalace/tusky/StatusListActivity.kt | 10 +- .../tusky/TabPreferenceActivity.kt | 44 +-- .../keylesspalace/tusky/ViewMediaActivity.kt | 90 +++--- .../announcements/AnnouncementsActivity.kt | 45 +-- .../components/compose/ComposeActivity.kt | 266 +++++++++--------- .../instancemute/InstanceListActivity.kt | 5 +- .../preference/PreferencesActivity.kt | 8 +- .../tusky/components/report/ReportActivity.kt | 22 +- .../scheduled/ScheduledTootActivity.kt | 51 ++-- .../tusky/components/search/SearchActivity.kt | 13 +- .../tusky/util/ViewBindingExtensions.kt | 15 + app/src/main/res/layout/activity_about.xml | 4 +- app/src/main/res/layout/activity_account.xml | 67 ++++- .../main/res/layout/activity_account_list.xml | 5 +- .../res/layout/activity_announcements.xml | 4 +- .../main/res/layout/activity_edit_profile.xml | 4 +- app/src/main/res/layout/activity_filters.xml | 5 +- app/src/main/res/layout/activity_license.xml | 6 +- app/src/main/res/layout/activity_lists.xml | 8 +- .../res/layout/activity_modal_timeline.xml | 5 +- .../main/res/layout/activity_preferences.xml | 5 +- app/src/main/res/layout/activity_report.xml | 5 +- .../res/layout/activity_scheduled_toot.xml | 7 +- .../main/res/layout/activity_statuslist.xml | 5 +- .../res/layout/activity_tab_preference.xml | 5 +- app/src/main/res/layout/activity_view_tag.xml | 1 - .../main/res/layout/view_account_moved.xml | 61 ---- 37 files changed, 741 insertions(+), 729 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt delete mode 100644 app/src/main/res/layout/view_account_moved.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt index 480954251..ada7af365 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -9,19 +9,20 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.text.util.Linkify import android.widget.TextView +import com.keylesspalace.tusky.databinding.ActivityAboutBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.util.CustomURLSpan import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.activity_about.* -import kotlinx.android.synthetic.main.toolbar_basic.* class AboutActivity : BottomSheetActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_about) - setSupportActionBar(toolbar) + val binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -29,26 +30,24 @@ class AboutActivity : BottomSheetActivity(), Injectable { setTitle(R.string.about_title_activity) - versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) + binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) if(BuildConfig.CUSTOM_INSTANCE.isBlank()) { - aboutPoweredByTusky.hide() + binding.aboutPoweredByTusky.hide() } - aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) - aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) - aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) + binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) + binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) + binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) - tuskyProfileButton.setOnClickListener { + binding.tuskyProfileButton.setOnClickListener { viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) } - aboutLicensesButton.setOnClickListener { + binding.aboutLicensesButton.setOnClickListener { startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) } - } - } private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { @@ -73,5 +72,4 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { setText(builder) linksClickable = true movementMethod = LinkMovementMethod.getInstance() - } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 397b7f64e..57f384751 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -50,6 +50,7 @@ import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.adapter.AccountFieldAdapter import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.databinding.ActivityAccountBinding import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship @@ -63,8 +64,6 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewmodel.AccountViewModel import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_account.* -import kotlinx.android.synthetic.main.view_account_moved.* import java.text.NumberFormat import javax.inject.Inject import kotlin.math.abs @@ -78,6 +77,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private val viewModel: AccountViewModel by viewModels { viewModelFactory } + private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate) + private lateinit var accountFieldAdapter : AccountFieldAdapter private var followState: FollowState = FollowState.NOT_FOLLOWING @@ -118,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI super.onCreate(savedInstanceState) loadResources() makeNotificationBarTransparent() - setContentView(R.layout.activity_account) + setContentView(binding.root) // Obtain information to fill out the profile. viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) @@ -136,9 +137,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.isSelf) { updateButtons() - saveNoteInfo.hide() + binding.saveNoteInfo.hide() } else { - saveNoteInfo.visibility = View.INVISIBLE + binding.saveNoteInfo.visibility = View.INVISIBLE } } @@ -158,16 +159,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI */ private fun setupAccountViews() { // Initialise the default UI states. - accountFloatingActionButton.hide() - accountFollowButton.hide() - accountMuteButton.hide() - accountFollowsYouTextView.hide() + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountFollowsYouTextView.hide() // setup the RecyclerView for the account fields accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) - accountFieldList.isNestedScrollingEnabled = false - accountFieldList.layoutManager = LinearLayoutManager(this) - accountFieldList.adapter = accountFieldAdapter + binding.accountFieldList.isNestedScrollingEnabled = false + binding.accountFieldList.layoutManager = LinearLayoutManager(this) + binding.accountFieldList.adapter = accountFieldAdapter val accountListClickListener = { v: View -> @@ -179,15 +180,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) startActivityWithSlideInAnimation(accountListIntent) } - accountFollowers.setOnClickListener(accountListClickListener) - accountFollowing.setOnClickListener(accountListClickListener) + binding.accountFollowers.setOnClickListener(accountListClickListener) + binding.accountFollowing.setOnClickListener(accountListClickListener) - accountStatuses.setOnClickListener { + binding.accountStatuses.setOnClickListener { // Make nice ripple effect on tab - accountTabLayout.getTabAt(0)!!.select() - val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) + binding.accountTabLayout.getTabAt(0)!!.select() + val poorTabView = (binding.accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) poorTabView.isPressed = true - accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) + binding.accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) } // If wellbeing mode is enabled, follow stats and posts count should be hidden @@ -195,11 +196,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) if (wellbeingEnabled) { - accountStatuses.hide() - accountFollowers.hide() - accountFollowing.hide() + binding.accountStatuses.hide() + binding.accountFollowers.hide() + binding.accountFollowing.hide() } - } /** @@ -209,19 +209,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI // Setup the tabs and timeline pager. adapter = AccountPagerAdapter(this, viewModel.accountId) - accountFragmentViewPager.adapter = adapter - accountFragmentViewPager.offscreenPageLimit = 2 + binding.accountFragmentViewPager.adapter = adapter + binding.accountFragmentViewPager.offscreenPageLimit = 2 val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) - TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { tab, position -> + TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position -> tab.text = pageTitles[position] }.attach() val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) - accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) - accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { tab?.position?.let { position -> (adapter.getFragment(position) as? ReselectableFragment)?.onReselect() @@ -237,17 +237,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun setupToolbar() { // set toolbar top margin according to system window insets - accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> + binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> val top = insets.systemWindowInsetTop - val toolbarParams = accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams toolbarParams.topMargin = top insets.consumeSystemWindowInsets() } // Setup the toolbar. - setSupportActionBar(accountToolbar) + setSupportActionBar(binding.accountToolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -258,9 +258,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT) - accountToolbar.background = toolbarBackground + binding.accountToolbar.background = toolbarBackground - accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) + binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply { fillColor = ColorStateList.valueOf(toolbarColor) @@ -269,10 +269,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) .build() } - accountAvatarImageView.background = avatarBackground + binding.accountAvatarImageView.background = avatarBackground // Add a listener to change the toolbar icon color when it enters/exits its collapsed state. - accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { + binding.accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { @@ -289,19 +289,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (hideFab && !viewModel.isSelf && !blocking) { if (verticalOffset > oldOffset) { - accountFloatingActionButton.show() + binding.accountFloatingActionButton.show() } if (verticalOffset < oldOffset) { - accountFloatingActionButton.hide() + binding.accountFloatingActionButton.hide() } } val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize - accountAvatarImageView.scaleX = scaledAvatarSize - accountAvatarImageView.scaleY = scaledAvatarSize + binding.accountAvatarImageView.scaleX = scaledAvatarSize + binding.accountAvatarImageView.scaleY = scaledAvatarSize - accountAvatarImageView.visible(scaledAvatarSize > 0) + binding.accountAvatarImageView.visible(scaledAvatarSize > 0) val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f) @@ -311,7 +311,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor) - swipeToRefreshLayout.isEnabled = verticalOffset == 0 + binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0 } }) @@ -331,7 +331,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI when (it) { is Success -> onAccountChanged(it.data) is Error -> { - Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) .setAction(R.string.action_retry) { viewModel.refresh() } .show() } @@ -344,7 +344,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } if (it is Error) { - Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) .setAction(R.string.action_retry) { viewModel.refresh() } .show() } @@ -355,7 +355,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFieldAdapter.notifyDataSetChanged() }) viewModel.noteSaved.observe(this) { - saveNoteInfo.visible(it, View.INVISIBLE) + binding.saveNoteInfo.visible(it, View.INVISIBLE) } } @@ -363,32 +363,32 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI * Setup swipe to refresh layout */ private fun setupRefreshLayout() { - swipeToRefreshLayout.setOnRefreshListener { + binding.swipeToRefreshLayout.setOnRefreshListener { viewModel.refresh() adapter.refreshContent() } viewModel.isRefreshing.observe(this, { isRefreshing -> - swipeToRefreshLayout.isRefreshing = isRefreshing == true + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true }) - swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } private fun onAccountChanged(account: Account?) { loadedAccount = account ?: return val usernameFormatted = getString(R.string.status_username_format, account.username) - accountUsernameTextView.text = usernameFormatted - accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis) + binding.accountUsernameTextView.text = usernameFormatted + binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis) - LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) + val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) + LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.notifyDataSetChanged() - accountLockedImageView.visible(account.locked) - accountBadgeTextView.visible(account.bot) + binding.accountLockedImageView.visible(account.locked) + binding.accountBadgeTextView.visible(account.bot) updateAccountAvatar() updateToolbar() @@ -397,7 +397,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI updateAccountStats() invalidateOptionsMenu() - accountMuteButton.setOnClickListener { + binding.accountMuteButton.setOnClickListener { viewModel.unmuteAccount() updateMuteButton() } @@ -411,7 +411,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadAvatar( account.avatar, - accountAvatarImageView, + binding.accountAvatarImageView, resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), animateAvatar ) @@ -420,10 +420,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .asBitmap() .load(account.header) .centerCrop() - .into(accountHeaderImageView) + .into(binding.accountHeaderImageView) - accountAvatarImageView.setOnClickListener { avatarView -> + binding.accountAvatarImageView.setOnClickListener { avatarView -> val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) avatarView.transitionName = account.avatar @@ -440,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateToolbar() { loadedAccount?.let { account -> - val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis) + val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis) try { supportActionBar?.title = EmojiCompat.get().process(emojifiedName) @@ -457,28 +457,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateMovedAccount() { loadedAccount?.moved?.let { movedAccount -> - accountMovedView?.show() + binding.accountMovedView.show() - // necessary because accountMovedView is now replaced in layout hierachy - findViewById(R.id.accountMovedViewLayout).setOnClickListener { + binding.accountMovedView.setOnClickListener { onViewAccount(movedAccount.id) } - accountMovedDisplayName.text = movedAccount.name - accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) + binding.accountMovedDisplayName.text = movedAccount.name + binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(movedAccount.avatar, accountMovedAvatar, avatarRadius, animateAvatar) + loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar) - accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) + binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) // this is necessary because API 19 can't handle vector compound drawables val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate() val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) - accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) + binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) } } @@ -489,8 +488,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateRemoteAccount() { loadedAccount?.let { account -> if (account.isRemote()) { - accountRemoveView.show() - accountRemoveView.setOnClickListener { + binding.accountRemoveView.show() + binding.accountRemoveView.setOnClickListener { LinkHelper.openLink(account.url, this) } } @@ -503,13 +502,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateAccountStats() { loadedAccount?.let { account -> val numberFormat = NumberFormat.getNumberInstance() - accountFollowersTextView.text = numberFormat.format(account.followersCount) - accountFollowingTextView.text = numberFormat.format(account.followingCount) - accountStatusesTextView.text = numberFormat.format(account.statusesCount) + binding.accountFollowersTextView.text = numberFormat.format(account.followersCount) + binding.accountFollowingTextView.text = numberFormat.format(account.followingCount) + binding.accountStatusesTextView.text = numberFormat.format(account.statusesCount) - accountFloatingActionButton.setOnClickListener { mention() } + binding.accountFloatingActionButton.setOnClickListener { mention() } - accountFollowButton.setOnClickListener { + binding.accountFollowButton.setOnClickListener { if (viewModel.isSelf) { val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) startActivity(intent) @@ -552,14 +551,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val preferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) - accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) + binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) // because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call if(!viewModel.isSelf && followState == FollowState.FOLLOWING && (relation.subscribing != null || relation.notifying != null)) { - accountSubscribeButton.show() - accountSubscribeButton.setOnClickListener { + binding.accountSubscribeButton.show() + binding.accountSubscribeButton.setOnClickListener { viewModel.changeSubscribingState() } if(relation.notifying != null) @@ -569,12 +568,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } // remove the listener so it doesn't fire on non-user changes - accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + binding.accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) - accountNoteTextInputLayout.visible(relation.note != null) - accountNoteTextInputLayout.editText?.setText(relation.note) + binding.accountNoteTextInputLayout.visible(relation.note != null) + binding.accountNoteTextInputLayout.editText?.setText(relation.note) - accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) + binding.accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) updateButtons() } @@ -587,22 +586,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateFollowButton() { if (viewModel.isSelf) { - accountFollowButton.setText(R.string.action_edit_own_profile) + binding.accountFollowButton.setText(R.string.action_edit_own_profile) return } if (blocking) { - accountFollowButton.setText(R.string.action_unblock) + binding.accountFollowButton.setText(R.string.action_unblock) return } when (followState) { FollowState.NOT_FOLLOWING -> { - accountFollowButton.setText(R.string.action_follow) + binding.accountFollowButton.setText(R.string.action_follow) } FollowState.REQUESTED -> { - accountFollowButton.setText(R.string.state_follow_requested) + binding.accountFollowButton.setText(R.string.state_follow_requested) } FollowState.FOLLOWING -> { - accountFollowButton.setText(R.string.action_unfollow) + binding.accountFollowButton.setText(R.string.action_unfollow) } } updateSubscribeButton() @@ -610,23 +609,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateMuteButton() { if (muting) { - accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) + binding.accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) } else { - accountMuteButton.hide() + binding.accountMuteButton.hide() } } private fun updateSubscribeButton() { if(followState != FollowState.FOLLOWING) { - accountSubscribeButton.hide() + binding.accountSubscribeButton.hide() } if(subscribing) { - accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) - accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) } else { - accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) - accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) } } @@ -635,27 +634,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (loadedAccount?.moved == null) { - accountFollowButton.show() + binding.accountFollowButton.show() updateFollowButton() if (blocking || viewModel.isSelf) { - accountFloatingActionButton.hide() - accountMuteButton.hide() - accountSubscribeButton.hide() + binding.accountFloatingActionButton.hide() + binding.accountMuteButton.hide() + binding.accountSubscribeButton.hide() } else { - accountFloatingActionButton.show() + binding.accountFloatingActionButton.show() if (muting) - accountMuteButton.show() + binding.accountMuteButton.show() else - accountMuteButton.hide() + binding.accountMuteButton.hide() updateMuteButton() } } else { - accountFloatingActionButton.hide() - accountFollowButton.hide() - accountMuteButton.hide() - accountSubscribeButton.hide() + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountSubscribeButton.hide() } } @@ -833,7 +832,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun getActionButton(): FloatingActionButton? { return if (!viewModel.isSelf && !blocking) { - accountFloatingActionButton + binding.accountFloatingActionButton } else null } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index d592f0531..71118501c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -18,10 +18,10 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding import com.keylesspalace.tusky.fragment.AccountListFragment import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class AccountListActivity : BaseActivity(), HasAndroidInjector { @@ -41,12 +41,13 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_account_list) + val binding = ActivityAccountListBinding.inflate(layoutInflater) + setContentView(binding.root) val type = intent.getSerializableExtra(EXTRA_TYPE) as Type val id: String? = intent.getStringExtra(EXTRA_ID) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { when (type) { Type.BLOCKS -> setTitle(R.string.title_blocks) diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 64d952b99..3d7e03809 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -38,6 +38,7 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter +import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.* @@ -47,8 +48,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import com.theartofdev.edmodo.cropper.CropImage -import kotlinx.android.synthetic.main.activity_edit_profile.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class EditProfileActivity : BaseActivity(), Injectable { @@ -71,6 +70,8 @@ class EditProfileActivity : BaseActivity(), Injectable { private val viewModel: EditProfileViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityEditProfileBinding::inflate) + private var currentlyPicking: PickType = PickType.NOTHING private val accountFieldEditAdapter = AccountFieldEditAdapter() @@ -88,33 +89,33 @@ class EditProfileActivity : BaseActivity(), Injectable { currentlyPicking = PickType.valueOf(it) } - setContentView(R.layout.activity_edit_profile) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setTitle(R.string.title_edit_profile) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } - headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } + binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } + binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } - fieldList.layoutManager = LinearLayoutManager(this) - fieldList.adapter = accountFieldEditAdapter + binding.fieldList.layoutManager = LinearLayoutManager(this) + binding.fieldList.adapter = accountFieldEditAdapter val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE } - addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null) + binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null) - addFieldButton.setOnClickListener { + binding.addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { it.isVisible = false } - scrollView.post{ - scrollView.smoothScrollTo(0, it.bottom) + binding.scrollView.post{ + binding.scrollView.smoothScrollTo(0, it.bottom) } } @@ -126,12 +127,12 @@ class EditProfileActivity : BaseActivity(), Injectable { val me = profileRes.data if (me != null) { - displayNameEditText.setText(me.displayName) - noteEditText.setText(me.source?.note) - lockedCheckBox.isChecked = me.locked + binding.displayNameEditText.setText(me.displayName) + binding.noteEditText.setText(me.source?.note) + binding.lockedCheckBox.isChecked = me.locked accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) - addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS + binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS if(viewModel.avatarData.value == null) { Glide.with(this) @@ -141,19 +142,19 @@ class EditProfileActivity : BaseActivity(), Injectable { FitCenter(), RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) ) - .into(avatarPreview) + .into(binding.avatarPreview) } if(viewModel.headerData.value == null) { Glide.with(this) .load(me.header) - .into(headerPreview) + .into(binding.headerPreview) } } } is Error -> { - val snackbar = Snackbar.make(avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) + val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) snackbar.setAction(R.string.action_retry) { viewModel.obtainProfile() } @@ -169,14 +170,14 @@ class EditProfileActivity : BaseActivity(), Injectable { is Success -> { val instance = result.data if (instance?.maxBioChars != null && instance.maxBioChars > 0) { - noteEditTextLayout.counterMaxLength = instance.maxBioChars + binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars } } } } - observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true) - observeImage(viewModel.headerData, headerPreview, headerProgressBar, false) + observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true) + observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false) viewModel.saveData.observe(this, { when(it) { @@ -184,7 +185,7 @@ class EditProfileActivity : BaseActivity(), Injectable { finish() } is Loading -> { - saveProgressBar.visibility = View.VISIBLE + binding.saveProgressBar.visibility = View.VISIBLE } is Error -> { onSaveFailure(it.errorMessage) @@ -202,9 +203,9 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onStop() { super.onStop() if(!isFinishing) { - viewModel.updateProfile(displayNameEditText.text.toString(), - noteEditText.text.toString(), - lockedCheckBox.isChecked, + viewModel.updateProfile(binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, accountFieldEditAdapter.getFieldData()) } } @@ -268,7 +269,7 @@ class EditProfileActivity : BaseActivity(), Injectable { initiateMediaPicking() } else { endMediaPicking() - Snackbar.make(avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() } } } @@ -309,39 +310,38 @@ class EditProfileActivity : BaseActivity(), Injectable { return } - viewModel.save(displayNameEditText.text.toString(), - noteEditText.text.toString(), - lockedCheckBox.isChecked, + viewModel.save(binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, accountFieldEditAdapter.getFieldData(), this) } private fun onSaveFailure(msg: String?) { val errorMsg = msg ?: getString(R.string.error_media_upload_sending) - Snackbar.make(avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() - saveProgressBar.visibility = View.GONE + Snackbar.make(binding.avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() + binding.saveProgressBar.visibility = View.GONE } private fun beginMediaPicking() { when (currentlyPicking) { PickType.AVATAR -> { - avatarProgressBar.visibility = View.VISIBLE - avatarPreview.visibility = View.INVISIBLE - avatarButton.setImageDrawable(null) - + binding.avatarProgressBar.visibility = View.VISIBLE + binding.avatarPreview.visibility = View.INVISIBLE + binding.avatarButton.setImageDrawable(null) } PickType.HEADER -> { - headerProgressBar.visibility = View.VISIBLE - headerPreview.visibility = View.INVISIBLE - headerButton.setImageDrawable(null) + binding.headerProgressBar.visibility = View.VISIBLE + binding.headerPreview.visibility = View.INVISIBLE + binding.headerButton.setImageDrawable(null) } PickType.NOTHING -> { /* do nothing */ } } } private fun endMediaPicking() { - avatarProgressBar.visibility = View.GONE - headerProgressBar.visibility = View.GONE + binding.avatarProgressBar.visibility = View.GONE + binding.headerProgressBar.visibility = View.GONE currentlyPicking = PickType.NOTHING } @@ -402,7 +402,7 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun onResizeFailure() { - Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() endMediaPicking() } diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 0726b26e6..7e91db07d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -7,13 +7,13 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.ActivityFiltersBinding +import com.keylesspalace.tusky.databinding.DialogFilterBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.activity_filters.* -import kotlinx.android.synthetic.main.dialog_filter.* -import kotlinx.android.synthetic.main.toolbar_basic.* +import com.keylesspalace.tusky.util.viewBinding import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Callback @@ -28,13 +28,28 @@ class FiltersActivity: BaseActivity() { @Inject lateinit var eventHub: EventHub + private val binding by viewBinding(ActivityFiltersBinding::inflate) + private lateinit var context : String private lateinit var filters: MutableList - private lateinit var dialog: AlertDialog - companion object { - const val FILTERS_CONTEXT = "filters_context" - const val FILTERS_TITLE = "filters_title" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + binding.addFilterButton.setOnClickListener { + showAddFilterDialog() + } + + title = intent?.getStringExtra(FILTERS_TITLE) + context = intent?.getStringExtra(FILTERS_CONTEXT)!! + loadFilters() } private fun updateFilter(filter: Filter, itemIndex: Int) { @@ -101,52 +116,51 @@ class FiltersActivity: BaseActivity() { } private fun showAddFilterDialog() { - dialog = AlertDialog.Builder(this@FiltersActivity) + val binding = DialogFilterBinding.inflate(layoutInflater) + binding.phraseWholeWord.isChecked = true + AlertDialog.Builder(this@FiltersActivity) .setTitle(R.string.filter_addition_dialog_title) - .setView(R.layout.dialog_filter) + .setView(binding.root) .setPositiveButton(android.R.string.ok){ _, _ -> - createFilter(dialog.phraseEditText.text.toString(), dialog.phraseWholeWord.isChecked) + createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked) } .setNeutralButton(android.R.string.cancel, null) - .create() - dialog.show() - dialog.phraseWholeWord.isChecked = true + .show() } private fun setupEditDialogForItem(itemIndex: Int) { - dialog = AlertDialog.Builder(this@FiltersActivity) + val binding = DialogFilterBinding.inflate(layoutInflater) + val filter = filters[itemIndex] + binding.phraseEditText.setText(filter.phrase) + binding.phraseWholeWord.isChecked = filter.wholeWord + + AlertDialog.Builder(this@FiltersActivity) .setTitle(R.string.filter_edit_dialog_title) - .setView(R.layout.dialog_filter) + .setView(binding.root) .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> val oldFilter = filters[itemIndex] - val newFilter = Filter(oldFilter.id, dialog.phraseEditText.text.toString(), oldFilter.context, - oldFilter.expiresAt, oldFilter.irreversible, dialog.phraseWholeWord.isChecked) + val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context, + oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked) updateFilter(newFilter, itemIndex) } .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> deleteFilter(itemIndex) } .setNeutralButton(android.R.string.cancel, null) - .create() - dialog.show() - - // Need to show the dialog before referencing any elements from its view - val filter = filters[itemIndex] - dialog.phraseEditText.setText(filter.phrase) - dialog.phraseWholeWord.isChecked = filter.wholeWord + .show() } private fun refreshFilterDisplay() { - filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) - filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) } + binding.filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) + binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) } } private fun loadFilters() { - filterMessageView.hide() - filtersView.hide() - addFilterButton.hide() - filterProgressBar.show() + binding.filterMessageView.hide() + binding.filtersView.hide() + binding.addFilterButton.hide() + binding.filterProgressBar.show() api.getFilters().enqueue(object : Callback> { override fun onResponse(call: Call>, response: Response>) { @@ -156,52 +170,33 @@ class FiltersActivity: BaseActivity() { filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList() refreshFilterDisplay() - filtersView.show() - addFilterButton.show() - filterProgressBar.hide() + binding.filtersView.show() + binding.addFilterButton.show() + binding.filterProgressBar.hide() } else { - filterProgressBar.hide() - filterMessageView.show() - filterMessageView.setup(R.drawable.elephant_error, + binding.filterProgressBar.hide() + binding.filterMessageView.show() + binding.filterMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { loadFilters() } } } override fun onFailure(call: Call>, t: Throwable) { - filterProgressBar.hide() - filterMessageView.show() + binding.filterProgressBar.hide() + binding.filterMessageView.show() if (t is IOException) { - filterMessageView.setup(R.drawable.elephant_offline, + binding.filterMessageView.setup(R.drawable.elephant_offline, R.string.error_network) { loadFilters() } } else { - filterMessageView.setup(R.drawable.elephant_error, + binding.filterMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { loadFilters() } } } }) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_filters) - setupToolbarBackArrow() - addFilterButton.setOnClickListener { - showAddFilterDialog() - } - - title = intent?.getStringExtra(FILTERS_TITLE) - context = intent?.getStringExtra(FILTERS_CONTEXT)!! - loadFilters() + companion object { + const val FILTERS_CONTEXT = "filters_context" + const val FILTERS_TITLE = "filters_title" } - - private fun setupToolbarBackArrow() { - setSupportActionBar(toolbar) - supportActionBar?.run { - // Back button - setDisplayHomeAsUpEnabled(true) - setDisplayShowHomeEnabled(true) - } - } - } \ 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 d6cc7bcaa..406a4aafb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -19,23 +19,20 @@ import android.os.Bundle import androidx.annotation.RawRes import android.util.Log import android.widget.TextView +import com.keylesspalace.tusky.databinding.ActivityLicenseBinding import com.keylesspalace.tusky.util.IOUtils -import kotlinx.android.extensions.CacheImplementation -import kotlinx.android.extensions.ContainerOptions -import kotlinx.android.synthetic.main.activity_license.* -import kotlinx.android.synthetic.main.toolbar_basic.* import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader class LicenseActivity : BaseActivity() { - @ContainerOptions(cache = CacheImplementation.NO_CACHE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_license) + val binding = ActivityLicenseBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -43,7 +40,7 @@ class LicenseActivity : BaseActivity() { setTitle(R.string.title_licenses) - loadFileIntoTextView(R.raw.apache, licenseApacheTextView) + loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView) } @@ -67,7 +64,5 @@ class LicenseActivity : BaseActivity() { IOUtils.closeQuietly(br) textView.text = sb.toString() - } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index fa3c92c3d..04311a660 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -24,12 +24,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.* import androidx.recyclerview.widget.ListAdapter import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList @@ -47,8 +49,6 @@ import com.uber.autodispose.autoDispose import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.activity_lists.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject /** @@ -57,47 +57,42 @@ import javax.inject.Inject class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { - companion object { - @JvmStatic - fun newIntent(context: Context): Intent { - return Intent(context, ListsActivity::class.java) - } - } - @Inject lateinit var viewModelFactory: ViewModelFactory @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector - private lateinit var viewModel: ListsViewModel + private val viewModel: ListsViewModel by viewModels { viewModelFactory } + + private val binding by viewBinding(ActivityListsBinding::inflate) + private val adapter = ListsAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_lists) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.title_lists) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - listsRecycler.adapter = adapter - listsRecycler.layoutManager = LinearLayoutManager(this) - listsRecycler.addItemDecoration( + binding.listsRecycler.adapter = adapter + binding.listsRecycler.layoutManager = LinearLayoutManager(this) + binding.listsRecycler.addItemDecoration( DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) - viewModel = viewModelFactory.create(ListsViewModel::class.java) viewModel.state .observeOn(AndroidSchedulers.mainThread()) .autoDispose(from(this)) .subscribe(this::update) viewModel.retryLoading() - addListButton.setOnClickListener { + binding.addListButton.setOnClickListener { showlistNameDialog(null) } @@ -153,37 +148,36 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun update(state: ListsViewModel.State) { adapter.submitList(state.lists) - progressBar.visible(state.loadingState == LOADING) + binding.progressBar.visible(state.loadingState == LOADING) when (state.loadingState) { - INITIAL, LOADING -> messageView.hide() + INITIAL, LOADING -> binding.messageView.hide() ERROR_NETWORK -> { - messageView.show() - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { viewModel.retryLoading() } } ERROR_OTHER -> { - messageView.show() - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { viewModel.retryLoading() } } LOADED -> if (state.lists.isEmpty()) { - messageView.show() - messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) } else { - messageView.hide() + binding.messageView.hide() } } } private fun showMessage(@StringRes messageId: Int) { Snackbar.make( - listsRecycler, messageId, Snackbar.LENGTH_SHORT + binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT ).show() - } private fun onListSelected(listId: String) { @@ -215,8 +209,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { } } - override fun androidInjector() = dispatchingAndroidInjector - private object ListsDiffer : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { return oldItem.id == newItem.id @@ -273,4 +265,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { viewModel.renameList(listId, name.toString()) } } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 1eebcf69a..2ba797983 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -29,15 +29,12 @@ import androidx.appcompat.app.AlertDialog import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import com.bumptech.glide.Glide +import com.keylesspalace.tusky.databinding.ActivityLoginBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.ThemeUtils -import com.keylesspalace.tusky.util.getNonNullString -import com.keylesspalace.tusky.util.rickRoll -import com.keylesspalace.tusky.util.shouldRickRoll -import kotlinx.android.synthetic.main.activity_login.* +import com.keylesspalace.tusky.util.* import okhttp3.HttpUrl import retrofit2.Call import retrofit2.Callback @@ -49,6 +46,8 @@ class LoginActivity : BaseActivity(), Injectable { @Inject lateinit var mastodonApi: MastodonApi + private val binding by viewBinding(ActivityLoginBinding::inflate) + private lateinit var preferences: SharedPreferences private val oauthRedirectUri: String @@ -61,26 +60,26 @@ class LoginActivity : BaseActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) + setContentView(binding.root) if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { - domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) - domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) + binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) + binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) } if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { - Glide.with(loginLogo) + Glide.with(binding.loginLogo) .load(BuildConfig.CUSTOM_LOGO_URL) .placeholder(null) - .into(loginLogo) + .into(binding.loginLogo) } preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE) - loginButton.setOnClickListener { onButtonClick() } + binding.loginButton.setOnClickListener { onButtonClick() } - whatsAnInstanceTextView.setOnClickListener { + binding.whatsAnInstanceTextView.setOnClickListener { val dialog = AlertDialog.Builder(this) .setMessage(R.string.dialog_whats_an_instance) .setPositiveButton(R.string.action_close, null) @@ -90,11 +89,11 @@ class LoginActivity : BaseActivity(), Injectable { } if (isAdditionalLogin()) { - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowTitleEnabled(false) } else { - toolbar.visibility = View.GONE + binding.toolbar.visibility = View.GONE } } @@ -117,15 +116,15 @@ class LoginActivity : BaseActivity(), Injectable { */ private fun onButtonClick() { - loginButton.isEnabled = false + binding.loginButton.isEnabled = false - val domain = canonicalizeDomain(domainEditText.text.toString()) + val domain = canonicalizeDomain(binding.domainEditText.text.toString()) try { HttpUrl.Builder().host(domain).scheme("https").build() } catch (e: IllegalArgumentException) { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_invalid_domain) + binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain) return } @@ -138,8 +137,8 @@ class LoginActivity : BaseActivity(), Injectable { override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { - loginButton.isEnabled = true - domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) setLoading(false) Log.e(TAG, "App authentication failed. " + response.message()) return @@ -158,8 +157,8 @@ class LoginActivity : BaseActivity(), Injectable { } override fun onFailure(call: Call, t: Throwable) { - loginButton.isEnabled = true - domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) setLoading(false) Log.e(TAG, Log.getStackTraceString(t)) } @@ -190,7 +189,7 @@ class LoginActivity : BaseActivity(), Injectable { if (viewIntent.resolveActivity(packageManager) != null) { startActivity(viewIntent) } else { - domainEditText.error = getString(R.string.error_no_web_browser_found) + binding.domainEditText.error = getString(R.string.error_no_web_browser_found) setLoading(false) } } @@ -224,7 +223,7 @@ class LoginActivity : BaseActivity(), Injectable { onLoginSuccess(response.body()!!.accessToken, domain) } else { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) + binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), response.message())) @@ -233,7 +232,7 @@ class LoginActivity : BaseActivity(), Injectable { override fun onFailure(call: Call, t: Throwable) { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) + binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), t.message)) @@ -246,14 +245,14 @@ class LoginActivity : BaseActivity(), Injectable { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ setLoading(false) - domainTextInputLayout.error = getString(R.string.error_authorization_denied) + binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) Log.e(TAG, String.format("%s %s", getString(R.string.error_authorization_denied), error)) } else { // This case means a junk response was received somehow. setLoading(false) - domainTextInputLayout.error = getString(R.string.error_authorization_unknown) + binding.domainTextInputLayout.error = getString(R.string.error_authorization_unknown) } } else { // first show or user cancelled login @@ -263,12 +262,12 @@ class LoginActivity : BaseActivity(), Injectable { private fun setLoading(loadingState: Boolean) { if (loadingState) { - loginLoadingLayout.visibility = View.VISIBLE - loginInputLayout.visibility = View.GONE + binding.loginLoadingLayout.visibility = View.VISIBLE + binding.loginInputLayout.visibility = View.GONE } else { - loginLoadingLayout.visibility = View.GONE - loginInputLayout.visibility = View.VISIBLE - loginButton.isEnabled = true + binding.loginLoadingLayout.visibility = View.GONE + binding.loginInputLayout.visibility = View.VISIBLE + binding.loginButton.isEnabled = true } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 3b3af8ae3..7cbbf0c7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -59,6 +59,7 @@ 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.databinding.ActivityMainBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.entity.Account @@ -86,7 +87,6 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_main.* import javax.inject.Inject class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { @@ -108,6 +108,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje @Inject lateinit var draftHelper: DraftHelper + private val binding by viewBinding(ActivityMainBinding::inflate) + private lateinit var header: AccountHeaderView private var notificationTabPosition = 0 @@ -179,21 +181,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } } window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own - setContentView(R.layout.activity_main) + setContentView(binding.root) glide = Glide.with(this) - composeButton.setOnClickListener { + binding.composeButton.setOnClickListener { val composeIntent = Intent(applicationContext, ComposeActivity::class.java) startActivity(composeIntent) } val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) - mainToolbar.visible(!hideTopToolbar) + binding.mainToolbar.visible(!hideTopToolbar) loadDrawerAvatar(activeAccount.profilePictureUrl, true) - mainToolbar.menu.add(R.string.action_search).apply { + binding.mainToolbar.menu.add(R.string.action_search).apply { setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { sizeDp = 20 @@ -249,11 +251,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onBackPressed() { when { - mainDrawerLayout.isOpen -> { - mainDrawerLayout.close() + binding.mainDrawerLayout.isOpen -> { + binding.mainDrawerLayout.close() } - viewPager.currentItem != 0 -> { - viewPager.currentItem = 0 + binding.viewPager.currentItem != 0 -> { + binding.viewPager.currentItem = 0 } else -> { super.onBackPressed() @@ -264,10 +266,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { when (keyCode) { KeyEvent.KEYCODE_MENU -> { - if (mainDrawerLayout.isOpen) { - mainDrawerLayout.close() + if (binding.mainDrawerLayout.isOpen) { + binding.mainDrawerLayout.close() } else { - mainDrawerLayout.open() + binding.mainDrawerLayout.open() } return true } @@ -319,8 +321,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { - mainToolbar.setNavigationOnClickListener { - mainDrawerLayout.open() + binding.mainToolbar.setNavigationOnClickListener { + binding.mainDrawerLayout.open() } header = AccountHeaderView(this).apply { @@ -333,7 +335,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje descriptionRes = R.string.add_account_description iconicsIcon = GoogleMaterial.Icon.gmd_add }, 0) - attachToSliderView(mainDrawer) + attachToSliderView(binding.mainDrawer) dividerBelowHeader = false closeDrawerOnProfileListClick = true } @@ -369,7 +371,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }) - mainDrawer.apply { + binding.mainDrawer.apply { tintStatusBar = true addItems( primaryDrawerItem { @@ -464,7 +466,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje ) if (addSearchButton) { - mainDrawer.addItemsAtPosition(4, + binding.mainDrawer.addItemsAtPosition(4, primaryDrawerItem { nameRes = R.string.action_search iconicsIcon = GoogleMaterial.Icon.gmd_search @@ -478,7 +480,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } if (BuildConfig.DEBUG) { - mainDrawer.addItems( + binding.mainDrawer.addItems( secondaryDrawerItem { nameText = "debug" isEnabled = false @@ -490,7 +492,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) + super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState)) } private fun setupTabs(selectNotificationTab: Boolean) { @@ -498,21 +500,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) - (composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin - tabLayout.hide() - bottomTabLayout + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin + binding.tabLayout.hide() + binding.bottomTabLayout } else { - bottomNav.hide() - (viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 - (composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager - tabLayout + binding.bottomNav.hide() + (binding.viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager + binding.tabLayout } val tabs = accountManager.activeAccount!!.tabPreferences val adapter = MainPagerAdapter(tabs, this) - viewPager.adapter = adapter - TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() + binding.viewPager.adapter = adapter + TabLayoutMediator(activeTabLayout, binding.viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() activeTabLayout.removeAllTabs() for (i in tabs.indices) { val tab = activeTabLayout.newTab() @@ -533,10 +535,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) - viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true) - viewPager.isUserInputEnabled = enableSwipeForTabs + binding.viewPager.isUserInputEnabled = enableSwipeForTabs onTabSelectedListener?.let { activeTabLayout.removeOnTabSelectedListener(it) @@ -548,7 +550,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.clearNotificationsForActiveAccount(this@MainActivity, accountManager) } - mainToolbar.title = tabs[tab.position].title(this@MainActivity) + binding.mainToolbar.title = tabs[tab.position].title(this@MainActivity) } override fun onTabUnselected(tab: TabLayout.Tab) {} @@ -564,8 +566,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 - mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) - mainToolbar.setOnClickListener { + binding.mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) + binding.mainToolbar.setOnClickListener { (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() } @@ -659,7 +661,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) // Show follow requests in the menu, if this is a locked account. - if (me.locked && mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) { + if (me.locked && binding.mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) { val followRequestsItem = primaryDrawerItem { identifier = DRAWER_ITEM_FOLLOW_REQUESTS nameRes = R.string.action_view_follow_requests @@ -670,9 +672,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje startActivityWithSlideInAnimation(intent) } } - mainDrawer.addItemAtPosition(4, followRequestsItem) + binding.mainDrawer.addItemAtPosition(4, followRequestsItem) } else if (!me.locked) { - mainDrawer.removeItems(DRAWER_ITEM_FOLLOW_REQUESTS) + binding.mainDrawer.removeItems(DRAWER_ITEM_FOLLOW_REQUESTS) } updateProfiles() updateShortcut(this, accountManager.activeAccount!!) @@ -695,16 +697,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onLoadStarted(placeholder: Drawable?) { if (placeholder != null) { - mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } override fun onResourceReady(resource: Drawable, transition: Transition?) { - mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) } override fun onLoadCleared(placeholder: Drawable?) { if (placeholder != null) { - mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } }) @@ -726,7 +728,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun updateAnnouncementsBadge() { - mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) + binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) } private fun updateProfiles() { @@ -779,7 +781,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } - override fun getActionButton(): FloatingActionButton? = composeButton + override fun getActionButton(): FloatingActionButton? = binding.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 c3017b0c3..64c229179 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -4,43 +4,28 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.interfaces.ActionButtonActivity import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { - companion object { - private const val ARG_KIND = "kind" - private const val ARG_ARG = "arg" - - @JvmStatic - fun newIntent(context: Context, kind: TimelineFragment.Kind, - argument: String?): Intent { - val intent = Intent(context, ModalTimelineActivity::class.java) - intent.putExtra(ARG_KIND, kind) - intent.putExtra(ARG_ARG, argument) - return intent - } - - } - @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_modal_timeline) + val binding = ActivityModalTimelineBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) - val bar = supportActionBar - if (bar != null) { - bar.title = getString(R.string.title_list_timeline) - bar.setDisplayHomeAsUpEnabled(true) - bar.setDisplayShowHomeEnabled(true) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_list_timeline) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) } if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { @@ -57,4 +42,18 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn override fun androidInjector() = dispatchingAndroidInjector + companion object { + private const val ARG_KIND = "kind" + private const val ARG_ARG = "arg" + + @JvmStatic + fun newIntent(context: Context, kind: TimelineFragment.Kind, + argument: String?): Intent { + val intent = Intent(context, ModalTimelineActivity::class.java) + intent.putExtra(ARG_KIND, kind) + intent.putExtra(ARG_ARG, argument) + return intent + } + + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 9eba5bbe1..b2691ee94 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit +import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment.Kind @@ -27,9 +28,6 @@ import javax.inject.Inject import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.extensions.CacheImplementation -import kotlinx.android.extensions.ContainerOptions -import kotlinx.android.synthetic.main.toolbar_basic.* class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @@ -39,12 +37,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { private val kind: Kind get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) - @ContainerOptions(cache = CacheImplementation.NO_CACHE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_statuslist) + val binding = ActivityStatuslistBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) val title = if(kind == Kind.FAVOURITES) { R.string.title_favourites diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 2b61f141a..2fd599026 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -38,17 +38,17 @@ import com.keylesspalace.tusky.adapter.ListSelectionAdapter import com.keylesspalace.tusky.adapter.TabAdapter import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.onTextChanged +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_tab_preference.* -import kotlinx.android.synthetic.main.toolbar_basic.* import java.util.regex.Pattern import javax.inject.Inject @@ -59,6 +59,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene @Inject lateinit var eventHub: EventHub + private val binding by viewBinding(ActivityTabPreferenceBinding::inflate) + private lateinit var currentTabs: MutableList private lateinit var currentTabsAdapter: TabAdapter private lateinit var touchHelper: ItemTouchHelper @@ -73,9 +75,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_tab_preference) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { setTitle(R.string.title_tab_preferences) @@ -85,13 +87,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) - currentTabsRecyclerView.adapter = currentTabsAdapter - currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) - currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + binding.currentTabsRecyclerView.adapter = currentTabsAdapter + binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this) - addTabRecyclerView.adapter = addTabAdapter - addTabRecyclerView.layoutManager = LinearLayoutManager(this) + binding.addTabRecyclerView.adapter = addTabAdapter + binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { @@ -132,17 +134,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } }) - touchHelper.attachToRecyclerView(currentTabsRecyclerView) + touchHelper.attachToRecyclerView(binding.currentTabsRecyclerView) - actionButton.setOnClickListener { + binding.actionButton.setOnClickListener { toggleFab(true) } - scrim.setOnClickListener { + binding.scrim.setOnClickListener { toggleFab(false) } - maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) + binding.maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) updateAvailableTabs() } @@ -193,18 +195,18 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene private fun toggleFab(expand: Boolean) { val transition = MaterialContainerTransform().apply { - startView = if (expand) actionButton else sheet - val endView: View = if (expand) sheet else actionButton + startView = if (expand) binding.actionButton else binding.sheet + val endView: View = if (expand) binding.sheet else binding.actionButton this.endView = endView addTarget(endView) scrimColor = Color.TRANSPARENT setPathMotion(MaterialArcMotion()) } - TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition) - actionButton.visible(!expand) - sheet.visible(expand) - scrim.visible(expand) + TransitionManager.beginDelayedTransition(binding.root, transition) + binding.actionButton.visible(!expand) + binding.sheet.visible(expand) + binding.scrim.visible(expand) } private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { @@ -310,7 +312,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene addTabAdapter.updateData(addableTabs) - maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) + binding.maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT) } @@ -337,7 +339,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onBackPressed() { - if (actionButton.isVisible) { + if (binding.actionButton.isVisible) { super.onBackPressed() } else { toggleFab(false) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 5d90bd943..4d5a124cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -44,18 +44,19 @@ import androidx.viewpager2.widget.ViewPager2 import com.bumptech.glide.Glide import com.bumptech.glide.request.FutureTarget import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID +import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_view_media.* import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -65,27 +66,8 @@ import java.util.* typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { - companion object { - private const val EXTRA_ATTACHMENTS = "attachments" - private const val EXTRA_ATTACHMENT_INDEX = "index" - private const val EXTRA_SINGLE_IMAGE_URL = "single_image" - private const val TAG = "ViewMediaActivity" - @JvmStatic - fun newIntent(context: Context?, attachments: List, index: Int): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) - intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) - return intent - } - - @JvmStatic - fun newSingleImageIntent(context: Context, url: String): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) - return intent - } - } + private val binding by viewBinding(ActivityViewMediaBinding::inflate) var isToolbarVisible = true private set @@ -102,7 +84,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_view_media) + setContentView(binding.root) supportPostponeEnterTransition() @@ -125,24 +107,24 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener SingleImagePagerAdapter(this, imageUrl!!) } - viewPager.adapter = adapter - viewPager.setCurrentItem(initialPosition, false) - viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { + binding.viewPager.adapter = adapter + binding.viewPager.setCurrentItem(initialPosition, false) + binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - toolbar.title = getPageTitle(position) + binding.toolbar.title = getPageTitle(position) } }) // Setup the toolbar. - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) val actionBar = supportActionBar if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setDisplayShowHomeEnabled(true) actionBar.title = getPageTitle(initialPosition) } - toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } - toolbar.setOnMenuItemClickListener { item: MenuItem -> + binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } + binding.toolbar.setOnMenuItemClickListener { item: MenuItem -> when (item.itemId) { R.id.action_download -> requestDownloadMedia() R.id.action_open_status -> onOpenStatus() @@ -156,7 +138,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener window.statusBarColor = Color.BLACK window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { override fun onTransitionEnd(transition: Transition) { - adapter.onTransitionEnd(viewPager.currentItem) + adapter.onTransitionEnd(binding.viewPager.currentItem) window.sharedElementEnterTransition.removeListener(this) } }) @@ -165,7 +147,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.view_media_toolbar, menu) // We don't support 'open status' from single image views - menu?.findItem(R.id.action_open_status)?.isVisible = (attachments != null) + menu.findItem(R.id.action_open_status)?.isVisible = (attachments != null) return true } @@ -192,14 +174,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener val alpha = if (isToolbarVisible) 1.0f else 0.0f if (isToolbarVisible) { // If to be visible, need to make visible immediately and animate alpha - toolbar.alpha = 0.0f - toolbar.visibility = visibility + binding.toolbar.alpha = 0.0f + binding.toolbar.visibility = visibility } - toolbar.animate().alpha(alpha) + binding.toolbar.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - toolbar.visibility = visibility + binding.toolbar.visibility = visibility animation.removeListener(this) } }) @@ -214,7 +196,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } private fun downloadMedia() { - val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url + val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url val filename = Uri.parse(url).lastPathSegment Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show() @@ -230,18 +212,18 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { downloadMedia() } else { - showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } + showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } } } } private fun onOpenStatus() { - val attach = attachments!![viewPager.currentItem] + val attach = attachments!![binding.viewPager.currentItem] startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) } private fun copyLink() { - val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url + val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.setPrimaryClip(ClipData.newPlainText(null, url)) } @@ -256,7 +238,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener if (imageUrl != null) { shareImage(directory, imageUrl!!) } else { - val attachment = attachments!![viewPager.currentItem].attachment + val attachment = attachments!![binding.viewPager.currentItem].attachment when (attachment.type) { Attachment.Type.IMAGE -> shareImage(directory, attachment.url) Attachment.Type.AUDIO, @@ -280,7 +262,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener private fun shareImage(directory: File, url: String) { isCreating = true - progressBarShare.visibility = View.VISIBLE + binding.progressBarShare.visibility = View.VISIBLE invalidateOptionsMenu() val file = File(directory, getTemporaryMediaFilename("png")) val futureTask: FutureTarget = @@ -312,14 +294,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener Log.d(TAG, "Download image result: $result") isCreating = false invalidateOptionsMenu() - progressBarShare.visibility = View.GONE + binding.progressBarShare.visibility = View.GONE if (result) shareFile(file, "image/png") }, { error -> isCreating = false invalidateOptionsMenu() - progressBarShare.visibility = View.GONE + binding.progressBarShare.visibility = View.GONE Log.e(TAG, "Failed to download image", error) } ) @@ -342,6 +324,28 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener shareFile(file, mimeType) } + + companion object { + private const val EXTRA_ATTACHMENTS = "attachments" + private const val EXTRA_ATTACHMENT_INDEX = "index" + private const val EXTRA_SINGLE_IMAGE_URL = "single_image" + private const val TAG = "ViewMediaActivity" + + @JvmStatic + fun newIntent(context: Context?, attachments: List, index: Int): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) + intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) + return intent + } + + @JvmStatic + fun newSingleImageIntent(context: Context, url: String): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) + return intent + } + } } abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) { 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 ffd97191c..9f61bea86 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 @@ -30,13 +30,12 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.view.EmojiPicker -import kotlinx.android.synthetic.main.activity_announcements.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { @@ -46,6 +45,8 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityAnnouncementsBinding::inflate) + private lateinit var adapter: AnnouncementAdapter private val picker by lazy { EmojiPicker(this) } @@ -63,22 +64,22 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_announcements) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.title_announcements) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - announcementsList.setHasFixedSize(true) - announcementsList.layoutManager = LinearLayoutManager(this) + binding.announcementsList.setHasFixedSize(true) + binding.announcementsList.layoutManager = LinearLayoutManager(this) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - announcementsList.addItemDecoration(divider) + binding.announcementsList.addItemDecoration(divider) val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) @@ -86,31 +87,31 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) - announcementsList.adapter = adapter + binding.announcementsList.adapter = adapter viewModel.announcements.observe(this) { when (it) { is Success -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false if (it.data.isNullOrEmpty()) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) - errorMessageView.show() + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) + binding.errorMessageView.show() } else { - errorMessageView.hide() + binding.errorMessageView.hide() } adapter.updateList(it.data ?: listOf()) } is Loading -> { - errorMessageView.hide() + binding.errorMessageView.hide() } is Error -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false - errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { refreshAnnouncements() } - errorMessageView.show() + binding.errorMessageView.show() } } } @@ -120,12 +121,12 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, } viewModel.load() - progressBar.show() + binding.progressBar.show() } private fun refreshAnnouncements() { viewModel.load() - swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true } override fun openReactionPicker(announcementId: String, target: View) { 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 f3c00bde5..26e1d5d97 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 @@ -61,6 +61,7 @@ import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView +import com.keylesspalace.tusky.databinding.ActivityComposeBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.di.Injectable @@ -76,7 +77,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.activity_compose.* import java.io.File import java.io.IOException import java.util.* @@ -109,17 +109,20 @@ class ComposeActivity : BaseActivity(), private val viewModel: ComposeViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityComposeBinding::inflate) + private val maxUploadMediaNumber = 4 private var mediaCount = 0 public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val preferences = PreferenceManager.getDefaultSharedPreferences(this) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) if (theme == "black") { setTheme(R.style.TuskyDialogActivityBlackTheme) } - setContentView(R.layout.activity_compose) + setContentView(binding.root) setupActionBar() // do not do anything when not logged in, activity will be finished in super.onCreate() anyway @@ -135,10 +138,10 @@ class ComposeActivity : BaseActivity(), }, onRemove = this::removeMediaFromQueue ) - composeMediaPreviewBar.layoutManager = + binding.composeMediaPreviewBar.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - composeMediaPreviewBar.adapter = mediaAdapter - composeMediaPreviewBar.itemAnimator = null + binding.composeMediaPreviewBar.adapter = mediaAdapter + binding.composeMediaPreviewBar.itemAnimator = null subscribeToUpdates(mediaAdapter) setupButtons() @@ -154,11 +157,11 @@ class ComposeActivity : BaseActivity(), setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) val tootText = composeOptions?.tootText if (!tootText.isNullOrEmpty()) { - composeEditField.setText(tootText) + binding.composeEditField.setText(tootText) } if (!composeOptions?.scheduledAt.isNullOrEmpty()) { - composeScheduleView.setDateTime(composeOptions?.scheduledAt) + binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt) } setupComposeField(preferences, viewModel.startingText) @@ -198,14 +201,14 @@ class ComposeActivity : BaseActivity(), } if (shareBody.isNotBlank()) { - val start = composeEditField.selectionStart.coerceAtLeast(0) - val end = composeEditField.selectionEnd.coerceAtLeast(0) + val start = binding.composeEditField.selectionStart.coerceAtLeast(0) + val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) val left = min(start, end) val right = max(start, end) - composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + binding.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) + binding.composeEditField.text.insert(0, "\n") + binding.composeEditField.setSelection(0) } } } @@ -214,58 +217,58 @@ class ComposeActivity : BaseActivity(), private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { if (replyingStatusAuthor != null) { - composeReplyView.show() - composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) + binding.composeReplyView.show() + binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 } ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary) - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) - composeReplyView.setOnClickListener { - TransitionManager.beginDelayedTransition(composeReplyContentView.parent as ViewGroup) + binding.composeReplyView.setOnClickListener { + TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup) - if (composeReplyContentView.isVisible) { - composeReplyContentView.hide() - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) + if (binding.composeReplyContentView.isVisible) { + binding.composeReplyContentView.hide() + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) } else { - composeReplyContentView.show() + binding.composeReplyContentView.show() val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 } ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary) - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null) } } } - replyingStatusContent?.let { composeReplyContentView.text = it } + replyingStatusContent?.let { binding.composeReplyContentView.text = it } } private fun setupContentWarningField(startingContentWarning: String?) { if (startingContentWarning != null) { - composeContentWarningField.setText(startingContentWarning) + binding.composeContentWarningField.setText(startingContentWarning) } - composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } + binding.composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } } private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { - composeEditField.setOnCommitContentListener(this) + binding.composeEditField.setOnCommitContentListener(this) - composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } + binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } - composeEditField.setAdapter( + binding.composeEditField.setAdapter( ComposeAutoCompleteAdapter( this, preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) ) - composeEditField.setTokenizer(ComposeTokenizer()) + binding.composeEditField.setTokenizer(ComposeTokenizer()) - composeEditField.setText(startingText) - composeEditField.setSelection(composeEditField.length()) + binding.composeEditField.setText(startingText) + binding.composeEditField.setSelection(binding.composeEditField.length()) - val mentionColour = composeEditField.linkTextColors.defaultColor - highlightSpans(composeEditField.text, mentionColour) - composeEditField.afterTextChanged { editable -> + val mentionColour = binding.composeEditField.linkTextColors.defaultColor + highlightSpans(binding.composeEditField.text, mentionColour) + binding.composeEditField.afterTextChanged { editable -> highlightSpans(editable, mentionColour) updateVisibleCharactersLeft() } @@ -273,7 +276,7 @@ class ComposeActivity : BaseActivity(), // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { - composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } } @@ -282,7 +285,7 @@ class ComposeActivity : BaseActivity(), viewModel.instanceParams.observe { instanceData -> maximumTootCharacters = instanceData.maxChars updateVisibleCharactersLeft() - composeScheduleButton.visible(instanceData.supportsScheduled) + binding.composeScheduleButton.visible(instanceData.supportsScheduled) } viewModel.emoji.observe { emoji -> setEmojiList(emoji) } combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> @@ -296,19 +299,19 @@ class ComposeActivity : BaseActivity(), mediaAdapter.submitList(media) if (media.size != mediaCount) { mediaCount = media.size - composeMediaPreviewBar.visible(media.isNotEmpty()) + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) } } viewModel.poll.observe { poll -> - pollPreview.visible(poll != null) - poll?.let(pollPreview::setPoll) + binding.pollPreview.visible(poll != null) + poll?.let(binding.pollPreview::setPoll) } viewModel.scheduledAt.observe { scheduledAt -> if (scheduledAt == null) { - composeScheduleView.resetSchedule() + binding.composeScheduleView.resetSchedule() } else { - composeScheduleView.setDateTime(scheduledAt) + binding.composeScheduleView.setDateTime(scheduledAt) } updateScheduleButton() } @@ -316,7 +319,7 @@ class ComposeActivity : BaseActivity(), val active = poll == null && media!!.size != 4 && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) - enableButton(composeAddMediaButton, active, active) + enableButton(binding.composeAddMediaButton, active, active) enablePollButton(media.isNullOrEmpty()) }.subscribe() viewModel.uploadError.observe { @@ -324,52 +327,52 @@ class ComposeActivity : BaseActivity(), } viewModel.setupComplete.observe { // Focus may have changed during view model setup, ensure initial focus is on the edit field - composeEditField.requestFocus() + binding.composeEditField.requestFocus() } } } private fun setupButtons() { - composeOptionsBottomSheet.listener = this + binding.composeOptionsBottomSheet.listener = this - composeOptionsBehavior = BottomSheetBehavior.from(composeOptionsBottomSheet) - addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet) - scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) - emojiBehavior = BottomSheetBehavior.from(emojiView) + composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet) + addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet) + scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) + emojiBehavior = BottomSheetBehavior.from(binding.emojiView) - enableButton(composeEmojiButton, clickable = false, colorActive = false) + enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) // Setup the interface buttons. - composeTootButton.setOnClickListener { onSendClicked() } - composeAddMediaButton.setOnClickListener { openPickDialog() } - composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } - composeContentWarningButton.setOnClickListener { onContentWarningChanged() } - composeEmojiButton.setOnClickListener { showEmojis() } - composeHideMediaButton.setOnClickListener { toggleHideMedia() } - composeScheduleButton.setOnClickListener { onScheduleClick() } - composeScheduleView.setResetOnClickListener { resetSchedule() } - composeScheduleView.setListener(this) - atButton.setOnClickListener { atButtonClicked() } - hashButton.setOnClickListener { hashButtonClicked() } + binding.composeTootButton.setOnClickListener { onSendClicked() } + binding.composeAddMediaButton.setOnClickListener { openPickDialog() } + binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } + binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() } + binding.composeEmojiButton.setOnClickListener { showEmojis() } + binding.composeHideMediaButton.setOnClickListener { toggleHideMedia() } + binding.composeScheduleButton.setOnClickListener { onScheduleClick() } + binding.composeScheduleView.setResetOnClickListener { resetSchedule() } + binding.composeScheduleView.setListener(this) + binding.atButton.setOnClickListener { atButtonClicked() } + binding.hashButton.setOnClickListener { hashButtonClicked() } val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } - actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) + binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } - actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) + binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 } - addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) + binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) - actionPhotoTake.setOnClickListener { initiateCameraApp() } - actionPhotoPick.setOnClickListener { onMediaPick() } - addPollTextActionTextView.setOnClickListener { openPollDialog() } + binding.actionPhotoTake.setOnClickListener { initiateCameraApp() } + binding.actionPhotoPick.setOnClickListener { onMediaPick() } + binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } } private fun setupActionBar() { - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) supportActionBar?.run { title = null setDisplayHomeAsUpEnabled(true) @@ -388,40 +391,40 @@ class ComposeActivity : BaseActivity(), val animateAvatars = preferences.getBoolean("animateGifAvatars", false) loadAvatar( activeAccount.profilePictureUrl, - composeAvatar, + binding.composeAvatar, avatarSize / 8, animateAvatars ) - composeAvatar.contentDescription = getString(R.string.compose_active_account_description, + binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description, activeAccount.fullName) } private fun replaceTextAtCaret(text: CharSequence) { // If you select "backward" in an editable, you get SelectionStart > SelectionEnd - val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) - val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) - val textToInsert = if (start > 0 && !composeEditField.text[start - 1].isWhitespace()) { + val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) + val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) + val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) { " $text" } else { text } - composeEditField.text.replace(start, end, textToInsert) + binding.composeEditField.text.replace(start, end, textToInsert) // Set the cursor after the inserted text - composeEditField.setSelection(start + text.length) + binding.composeEditField.setSelection(start + text.length) } fun prependSelectedWordsWith(text: CharSequence) { // If you select "backward" in an editable, you get SelectionStart > SelectionEnd - val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) - val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) - val editorText = composeEditField.text + val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) + val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) + val editorText = binding.composeEditField.text if (start == end) { // No selection, just insert text at caret editorText.insert(start, text) // Set the cursor after the inserted text - composeEditField.setSelection(start + text.length) + binding.composeEditField.setSelection(start + text.length) } else { var wasWord: Boolean var isWord = end < editorText.length && !Character.isWhitespace(editorText[end]) @@ -447,7 +450,7 @@ class ComposeActivity : BaseActivity(), } // Keep the same text (including insertions) selected - composeEditField.setSelection(start, newEnd) + binding.composeEditField.setSelection(start, newEnd) } } @@ -466,7 +469,7 @@ class ComposeActivity : BaseActivity(), } private fun displayTransientError(@StringRes stringId: Int) { - val bar = Snackbar.make(activityCompose, stringId, Snackbar.LENGTH_LONG) + val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG) //necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.show() @@ -478,49 +481,49 @@ class ComposeActivity : BaseActivity(), private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { if (viewModel.media.value.isNullOrEmpty()) { - composeHideMediaButton.hide() + binding.composeHideMediaButton.hide() } else { - composeHideMediaButton.show() + binding.composeHideMediaButton.show() @ColorInt val color = if (contentWarningShown) { - composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) - composeHideMediaButton.isClickable = false + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + binding.composeHideMediaButton.isClickable = false ContextCompat.getColor(this, R.color.transparent_tusky_blue) } else { - composeHideMediaButton.isClickable = true + binding.composeHideMediaButton.isClickable = true if (markMediaSensitive) { - composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) ContextCompat.getColor(this, R.color.tusky_blue) } else { - composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) + binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } } - composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } } private fun updateScheduleButton() { - @ColorInt val color = if (composeScheduleView.time == null) { + @ColorInt val color = if (binding.composeScheduleView.time == null) { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } else { ContextCompat.getColor(this, R.color.tusky_blue) } - composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } private fun enableButtons(enable: Boolean) { - composeAddMediaButton.isClickable = enable - composeToggleVisibilityButton.isClickable = enable - composeEmojiButton.isClickable = enable - composeHideMediaButton.isClickable = enable - composeScheduleButton.isClickable = enable - composeTootButton.isEnabled = enable + binding.composeAddMediaButton.isClickable = enable + binding.composeToggleVisibilityButton.isClickable = enable + binding.composeEmojiButton.isClickable = enable + binding.composeHideMediaButton.isClickable = enable + binding.composeScheduleButton.isClickable = enable + binding.composeTootButton.isEnabled = enable } private fun setStatusVisibility(visibility: Status.Visibility) { - composeOptionsBottomSheet.setStatusVisibility(visibility) - composeTootButton.setStatusVisibility(visibility) + binding.composeOptionsBottomSheet.setStatusVisibility(visibility) + binding.composeTootButton.setStatusVisibility(visibility) val iconRes = when (visibility) { Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp @@ -529,7 +532,7 @@ class ComposeActivity : BaseActivity(), Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp else -> R.drawable.ic_lock_open_24dp } - composeToggleVisibilityButton.setImageResource(iconRes) + binding.composeToggleVisibilityButton.setImageResource(iconRes) } private fun showComposeOptions() { @@ -545,7 +548,7 @@ class ComposeActivity : BaseActivity(), private fun onScheduleClick() { if (viewModel.scheduledAt.value == null) { - composeScheduleView.openPickDateDialog() + binding.composeScheduleView.openPickDateDialog() } else { showScheduleView() } @@ -563,7 +566,7 @@ class ComposeActivity : BaseActivity(), } private fun showEmojis() { - emojiView.adapter?.let { + binding.emojiView.adapter?.let { if (it.itemCount == 0) { val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain) Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() @@ -626,10 +629,10 @@ class ComposeActivity : BaseActivity(), val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams.setMargins(margin, margin, margin, marginBottom) - pollPreview.layoutParams = layoutParams + binding.pollPreview.layoutParams = layoutParams - pollPreview.setOnClickListener { - val popup = PopupMenu(this, pollPreview) + binding.pollPreview.setOnClickListener { + val popup = PopupMenu(this, binding.pollPreview) val editId = 1 val removeId = 2 popup.menu.add(0, editId, 0, R.string.edit_poll) @@ -647,7 +650,7 @@ class ComposeActivity : BaseActivity(), private fun removePoll() { viewModel.poll.value = null - pollPreview.hide() + binding.pollPreview.hide() } override fun onVisibilityChanged(visibility: Status.Visibility) { @@ -658,39 +661,39 @@ class ComposeActivity : BaseActivity(), @VisibleForTesting fun calculateTextLength(): Int { var offset = 0 - val urlSpans = composeEditField.urls + val urlSpans = binding.composeEditField.urls if (urlSpans != null) { for (span in urlSpans) { offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) } } - var length = composeEditField.length() - offset + var length = binding.composeEditField.length() - offset if (viewModel.showContentWarning.value!!) { - length += composeContentWarningField.length() + length += binding.composeContentWarningField.length() } return length } private fun updateVisibleCharactersLeft() { val remainingLength = maximumTootCharacters - calculateTextLength() - composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) + binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) val textColor = if (remainingLength < 0) { ContextCompat.getColor(this, R.color.tusky_red) } else { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } - composeCharactersLeftView.setTextColor(textColor) + binding.composeCharactersLeftView.setTextColor(textColor) } private fun onContentWarningChanged() { - val showWarning = composeContentWarningBar.isGone + val showWarning = binding.composeContentWarningBar.isGone viewModel.contentWarningChanged(showWarning) updateVisibleCharactersLeft() } private fun verifyScheduledTime(): Boolean { - return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value)) + return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)) } private fun onSendClicked() { @@ -725,14 +728,14 @@ class ComposeActivity : BaseActivity(), private fun sendStatus() { enableButtons(false) - val contentText = composeEditField.text.toString() + val contentText = binding.composeEditField.text.toString() var spoilerText = "" if (viewModel.showContentWarning.value!!) { - spoilerText = composeContentWarningField.text.toString() + spoilerText = binding.composeContentWarningField.text.toString() } val characterCount = calculateTextLength() if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) { - composeEditField.error = getString(R.string.error_empty) + binding.composeEditField.error = getString(R.string.error_empty) enableButtons(true) } else if (characterCount <= maximumTootCharacters) { if (viewModel.media.value!!.isNotEmpty()) { @@ -747,7 +750,7 @@ class ComposeActivity : BaseActivity(), }) } else { - composeEditField.error = getString(R.string.error_compose_character_limit) + binding.composeEditField.error = getString(R.string.error_compose_character_limit) enableButtons(true) } } @@ -758,7 +761,7 @@ class ComposeActivity : BaseActivity(), if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initiateMediaPicking() } else { - val bar = Snackbar.make(activityCompose, R.string.error_media_upload_permission, + val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, Snackbar.LENGTH_SHORT).apply { } @@ -813,12 +816,12 @@ class ComposeActivity : BaseActivity(), } private fun enablePollButton(enable: Boolean) { - addPollTextActionTextView.isEnabled = enable + binding.addPollTextActionTextView.isEnabled = enable val textColor = ThemeUtils.getColor(this, if (enable) android.R.attr.textColorTertiary else R.attr.textColorDisabled) - addPollTextActionTextView.setTextColor(textColor) - addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) + binding.addPollTextActionTextView.setTextColor(textColor) + binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) } private fun removeMediaFromQueue(item: QueuedMedia) { @@ -879,19 +882,18 @@ class ComposeActivity : BaseActivity(), } private fun showContentWarning(show: Boolean) { - TransitionManager.beginDelayedTransition(composeContentWarningBar.parent as ViewGroup) + TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup) @ColorInt val color = if (show) { - composeContentWarningBar.show() - composeContentWarningField.setSelection(composeContentWarningField.text.length) - composeContentWarningField.requestFocus() + binding.composeContentWarningBar.show() + binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length) + binding.composeContentWarningField.requestFocus() ContextCompat.getColor(this, R.color.tusky_blue) } else { - composeContentWarningBar.hide() - composeEditField.requestFocus() + binding.composeContentWarningBar.hide() + binding.composeEditField.requestFocus() ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } - composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) - + binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -939,8 +941,8 @@ class ComposeActivity : BaseActivity(), } private fun handleCloseButton() { - val contentText = composeEditField.text.toString() - val contentWarning = composeContentWarningField.text.toString() + val contentText = binding.composeEditField.text.toString() + val contentWarning = binding.composeContentWarningField.text.toString() if (viewModel.didChange(contentText, contentWarning)) { AlertDialog.Builder(this) .setMessage(R.string.compose_save_draft) @@ -974,8 +976,8 @@ class ComposeActivity : BaseActivity(), private fun setEmojiList(emojiList: List?) { if (emojiList != null) { - emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) - enableButton(composeEmojiButton, true, emojiList.isNotEmpty()) + binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) + enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } } 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 ca04f9c70..7253112a1 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 @@ -4,10 +4,10 @@ import android.os.Bundle import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -import kotlinx.android.synthetic.main.toolbar_basic.* class InstanceListActivity: BaseActivity(), HasAndroidInjector { @@ -16,9 +16,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val binding = ActivityAccountListBinding.inflate(layoutInflater) setContentView(R.layout.activity_account_list) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { setTitle(R.string.title_domain_mutes) setDisplayHomeAsUpEnabled(true) 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 7bb8766ac..f1a076159 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 @@ -28,12 +28,12 @@ import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.getNonNullString import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, @@ -48,12 +48,12 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference private var restartActivitiesOnExit: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_preferences) + val binding = ActivityPreferencesBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) 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 2c7f2d464..57c9214cf 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 @@ -22,11 +22,11 @@ import androidx.activity.viewModels import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter +import com.keylesspalace.tusky.databinding.ActivityReportBinding import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_report.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @@ -39,6 +39,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { private val viewModel: ReportViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityReportBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val accountId = intent?.getStringExtra(ACCOUNT_ID) @@ -50,9 +52,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) - setContentView(R.layout.activity_report) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.report_username_format, viewModel.accountUserName) @@ -69,8 +71,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun initViewPager() { - wizard.isUserInputEnabled = false - wizard.adapter = ReportPagerAdapter(this) + binding.wizard.isUserInputEnabled = false + binding.wizard.adapter = ReportPagerAdapter(this) } private fun subscribeObservables() { @@ -96,18 +98,18 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun showPreviousScreen() { - when (wizard.currentItem) { + when (binding.wizard.currentItem) { 0 -> closeScreen() 1 -> showStatusesPage() } } private fun showDonePage() { - wizard.currentItem = 2 + binding.wizard.currentItem = 2 } private fun showNotesPage() { - wizard.currentItem = 1 + binding.wizard.currentItem = 1 } private fun closeScreen() { @@ -115,7 +117,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun showStatusesPage() { - wizard.currentItem = 0 + binding.wizard.currentItem = 0 } companion object { 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 40a67a4eb..4af5771c1 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,20 +18,19 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.activity_scheduled_toot.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { @@ -39,31 +38,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec @Inject lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: ScheduledTootViewModel + private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } private val adapter = ScheduledTootAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_scheduled_toot) - setSupportActionBar(toolbar) + val binding = ActivityScheduledTootBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { title = getString(R.string.title_scheduled_toot) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - scheduledTootList.setHasFixedSize(true) - scheduledTootList.layoutManager = LinearLayoutManager(this) + binding.scheduledTootList.setHasFixedSize(true) + binding.scheduledTootList.layoutManager = LinearLayoutManager(this) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - scheduledTootList.addItemDecoration(divider) - scheduledTootList.adapter = adapter - - viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java] + binding.scheduledTootList.addItemDecoration(divider) + binding.scheduledTootList.adapter = adapter viewModel.data.observe(this) { adapter.submitList(it) @@ -72,31 +71,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec viewModel.networkState.observe(this) { (status) -> when(status) { Status.SUCCESS -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false if(viewModel.data.value?.loadedCount == 0) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) - errorMessageView.show() + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + binding.errorMessageView.show() } else { - errorMessageView.hide() + binding.errorMessageView.hide() } } Status.RUNNING -> { - errorMessageView.hide() + binding.errorMessageView.hide() if(viewModel.data.value?.loadedCount ?: 0 > 0) { - swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true } else { - progressBar.show() + binding.progressBar.show() } } Status.FAILED -> { if(viewModel.data.value?.loadedCount ?: 0 >= 0) { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false - errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { refreshStatuses() } - errorMessageView.show() + binding.errorMessageView.show() } } } 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 be705637d..7208b0388 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 @@ -26,10 +26,11 @@ import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter +import com.keylesspalace.tusky.databinding.ActivitySearchBinding import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_search.* import javax.inject.Inject class SearchActivity : BottomSheetActivity(), HasAndroidInjector { @@ -41,10 +42,12 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { private val viewModel: SearchViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivitySearchBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_search) - setSupportActionBar(toolbar) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -55,9 +58,9 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { } private fun setupPages() { - pages.adapter = SearchPagerAdapter(this) + binding.pages.adapter = SearchPagerAdapter(this) - TabLayoutMediator(tabs, pages) { + TabLayoutMediator(binding.tabs, binding.pages) { tab, position -> tab.text = getPageTitle(position) }.attach() diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt new file mode 100644 index 000000000..9558b03f9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -0,0 +1,15 @@ +package com.keylesspalace.tusky.util + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding + +/** + * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c + */ + +inline fun AppCompatActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T +) = lazy(LazyThreadSafetyMode.NONE) { + bindingInflater(layoutInflater) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 2d55b693e..43c7f9ae4 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -6,7 +6,9 @@ android:layout_height="match_parent" tools:context="com.keylesspalace.tusky.AboutActivity"> - + - + - + android:visibility="gone" + app:constraint_referenced_ids="accountMovedText,accountMovedAvatar,accountMovedDisplayName,accountMovedUsername" /> - + android:layout_marginTop="12dp" + android:drawablePadding="6dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountRemoveView" + tools:text="Account has moved" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> - + - + - + - + + tools:context=".LicenseActivity"> - + - + + app:layout_constraintTop_toBottomOf="@id/includedToolbar" /> diff --git a/app/src/main/res/layout/activity_modal_timeline.xml b/app/src/main/res/layout/activity_modal_timeline.xml index 6801d904b..05de634d0 100644 --- a/app/src/main/res/layout/activity_modal_timeline.xml +++ b/app/src/main/res/layout/activity_modal_timeline.xml @@ -2,12 +2,13 @@ - + - + - + + tools:context=".components.scheduled.ScheduledTootActivity"> - + - + - + diff --git a/app/src/main/res/layout/view_account_moved.xml b/app/src/main/res/layout/view_account_moved.xml deleted file mode 100644 index fbbfe48ec..000000000 --- a/app/src/main/res/layout/view_account_moved.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file From 22bed19d904385740c5230261411e468255c463c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Mar 2021 19:06:05 +0100 Subject: [PATCH 11/41] migrating to ViewBinding part 3: EmojiPreference (#2094) --- .../components/preference/EmojiPreference.kt | 145 ++++++++---------- .../tusky/util/EmojiCompatFont.kt | 8 +- app/src/main/res/layout/item_emoji_pref.xml | 33 ++-- 3 files changed, 83 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index c0e538a9c..2d32723fc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -8,14 +8,23 @@ import android.os.Build import android.util.Log import android.view.LayoutInflater import android.view.View -import android.widget.* +import android.widget.RadioButton +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceManager import com.keylesspalace.tusky.R import com.keylesspalace.tusky.SplashActivity +import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding +import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import okhttp3.OkHttpClient @@ -50,94 +59,85 @@ class EmojiPreference( } override fun onClick() { - val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) - viewIds.forEachIndexed { index, viewId -> - setupItem(view.findViewById(viewId), FONTS[index]) - } + val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context)) + + setupItem(BLOBMOJI, binding.itemBlobmoji) + setupItem(TWEMOJI, binding.itemTwemoji) + setupItem(NOTOEMOJI, binding.itemNotoemoji) + setupItem(SYSTEM_DEFAULT, binding.itemNomoji) + AlertDialog.Builder(context) - .setView(view) + .setView(binding.root) .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } .setNegativeButton(android.R.string.cancel, null) .show() } - private fun setupItem(container: View, font: EmojiCompatFont) { - val title: TextView = container.findViewById(R.id.emojicompat_name) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb) - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) - + private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // Initialize all the views - title.text = font.getDisplay(container.context) - caption.setText(font.caption) - thumb.setImageResource(font.img) + binding.emojiName.text = font.getDisplay(context) + binding.emojiCaption.setText(font.caption) + binding.emojiThumbnail.setImageResource(font.img) // There needs to be a list of all the radio buttons in order to uncheck them when one is selected - radioButtons.add(radio) - updateItem(font, container) + radioButtons.add(binding.emojiRadioButton) + updateItem(font, binding) // Set actions - download.setOnClickListener { startDownload(font, container) } - cancel.setOnClickListener { cancelDownload(font, container) } - radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } - container.setOnClickListener { containerView: View -> - select(font, containerView.findViewById(R.id.emojicompat_radio)) + binding.emojiDownload.setOnClickListener { startDownload(font, binding) } + binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) } + binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } + binding.root.setOnClickListener { + select(font, binding.emojiRadioButton) } } - private fun startDownload(font: EmojiCompatFont, container: View) { - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - + private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // Switch to downloading style - download.visibility = View.GONE - caption.visibility = View.INVISIBLE - progressBar.visibility = View.VISIBLE - progressBar.progress = 0 - cancel.visibility = View.VISIBLE + binding.emojiDownload.hide() + binding.emojiCaption.visibility = View.INVISIBLE + binding.emojiProgress.show() + binding.emojiProgress.progress = 0 + binding.emojiDownloadCancel.show() font.downloadFontFile(context, okHttpClient) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { progress -> // The progress is returned as a float between 0 and 1, or -1 if it could not determined if (progress >= 0) { - progressBar.isIndeterminate = false - val max = progressBar.max.toFloat() + binding.emojiProgress.isIndeterminate = false + val max = binding.emojiProgress.max.toFloat() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - progressBar.setProgress((max * progress).toInt(), true) + binding.emojiProgress.setProgress((max * progress).toInt(), true) } else { - progressBar.progress = (max * progress).toInt() + binding.emojiProgress.progress = (max * progress).toInt() } } else { - progressBar.isIndeterminate = true + binding.emojiProgress.isIndeterminate = true } }, { Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() - updateItem(font, container) + updateItem(font, binding) }, { - finishDownload(font, container) + finishDownload(font, binding) } ).also { downloadDisposables[font.id] = it } } - private fun cancelDownload(font: EmojiCompatFont, container: View) { - font.deleteDownloadedFile(container.context) + private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { + font.deleteDownloadedFile(context) downloadDisposables[font.id]?.dispose() downloadDisposables[font.id] = null - updateItem(font, container) + updateItem(font, binding) } - private fun finishDownload(font: EmojiCompatFont, container: View) { - select(font, container.findViewById(R.id.emojicompat_radio)) - updateItem(font, container) + private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { + select(font, binding.emojiRadioButton) + updateItem(font, binding) // Set the flag to restart the app (because an update has been downloaded) if (selected === original && currentNeedsUpdate) { updated = true @@ -153,54 +153,43 @@ class EmojiPreference( */ private fun select(font: EmojiCompatFont, radio: RadioButton) { selected = font - // Uncheck all the other buttons - for (other in radioButtons) { - if (other !== radio) { - other.isChecked = false - } + radioButtons.forEach { radioButton -> + radioButton.isChecked = radioButton == radio } - radio.isChecked = true } /** * Called when a "consistent" state is reached, i.e. it's not downloading the font * * @param font The font to be displayed - * @param container The ConstraintLayout containing the item + * @param binding The ItemEmojiPrefBinding to show the item in */ - private fun updateItem(font: EmojiCompatFont, container: View) { - // Assignments - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) - + private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // There's no download going on - progress.visibility = View.GONE - cancel.visibility = View.GONE - caption.visibility = View.VISIBLE + binding.emojiProgress.hide() + binding.emojiDownloadCancel.hide() + binding.emojiCaption.show() if (font.isDownloaded(context)) { // Make it selectable - download.visibility = View.GONE - radio.visibility = View.VISIBLE - container.isClickable = true + binding.emojiDownload.hide() + binding.emojiRadioButton.show() + binding.root.isClickable = true } else { // Make it downloadable - download.visibility = View.VISIBLE - radio.visibility = View.GONE - container.isClickable = false + binding.emojiDownload.show() + binding.emojiRadioButton.hide() + binding.root.isClickable = false } // Select it if necessary if (font === selected) { - radio.isChecked = true + binding.emojiRadioButton.isChecked = true // Update available if (!font.isDownloaded(context)) { currentNeedsUpdate = true } } else { - radio.isChecked = false + binding.emojiRadioButton.isChecked = false } } @@ -246,13 +235,5 @@ class EmojiPreference( companion object { private const val TAG = "EmojiPreference" - - // Please note that this array must sorted in the same way as the fonts. - private val viewIds = intArrayOf( - R.id.item_nomoji, - R.id.item_blobmoji, - R.id.item_twemoji, - R.id.item_notoemoji - ) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt index 4697a1e93..d0a0e443e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -256,27 +256,27 @@ class EmojiCompatFont( private const val CHUNK_SIZE = 4096L // The system font gets some special behavior... - private val SYSTEM_DEFAULT = EmojiCompatFont("system-default", + val SYSTEM_DEFAULT = EmojiCompatFont("system-default", "System Default", R.string.caption_systememoji, R.drawable.ic_emoji_34dp, "", "0") - private val BLOBMOJI = EmojiCompatFont("Blobmoji", + val BLOBMOJI = EmojiCompatFont("Blobmoji", "Blobmoji", R.string.caption_blobmoji, R.drawable.ic_blobmoji, "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", "12.0.0" ) - private val TWEMOJI = EmojiCompatFont("Twemoji", + val TWEMOJI = EmojiCompatFont("Twemoji", "Twemoji", R.string.caption_twemoji, R.drawable.ic_twemoji, "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", "12.0.0" ) - private val NOTOEMOJI = EmojiCompatFont("NotoEmoji", + val NOTOEMOJI = EmojiCompatFont("NotoEmoji", "Noto Emoji", R.string.caption_notoemoji, R.drawable.ic_notoemoji, diff --git a/app/src/main/res/layout/item_emoji_pref.xml b/app/src/main/res/layout/item_emoji_pref.xml index d8755a3dd..a10a5ffe7 100644 --- a/app/src/main/res/layout/item_emoji_pref.xml +++ b/app/src/main/res/layout/item_emoji_pref.xml @@ -2,7 +2,6 @@ + app:layout_constraintStart_toStartOf="@id/emojiName" + app:layout_constraintTop_toBottomOf="@id/emojiName" /> \ No newline at end of file From fc4b47aee4ba97983622db738e1409b45302427f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Mar 2021 19:24:01 +0100 Subject: [PATCH 12/41] migrating to ViewBinding part 4: Adapters (#2095) --- .../tusky/adapter/AccountFieldAdapter.kt | 45 +++--- .../tusky/adapter/AccountFieldEditAdapter.kt | 38 ++--- .../tusky/adapter/AccountSelectionAdapter.kt | 28 ++-- .../tusky/adapter/EmojiAdapter.kt | 46 +++--- .../tusky/adapter/FollowRequestViewHolder.kt | 52 ++++--- .../tusky/adapter/FollowRequestsAdapter.java | 8 +- .../tusky/adapter/HashtagViewHolder.kt | 16 -- .../tusky/adapter/ListSelectionAdapter.kt | 21 +-- .../tusky/adapter/MutesAdapter.java | 1 - .../tusky/adapter/NetworkStateViewHolder.kt | 21 ++- .../tusky/adapter/NotificationsAdapter.java | 8 +- .../tusky/adapter/PollAdapter.kt | 68 ++++----- .../adapter/PreviewPollOptionsAdapter.kt | 3 +- .../keylesspalace/tusky/adapter/TabAdapter.kt | 92 ++++++------ .../announcements/AnnouncementAdapter.kt | 141 +++++++++--------- .../compose/dialog/AddPollDialog.kt | 1 - .../compose/dialog}/AddPollOptionsAdapter.kt | 38 ++--- .../conversation/ConversationAdapter.kt | 28 +++- .../tusky/components/drafts/DraftsAdapter.kt | 10 +- .../adapter/DomainMutesAdapter.kt | 40 +++-- .../scheduled/ScheduledTootAdapter.kt | 46 ++---- .../search/adapter/SearchHashtagsAdapter.kt | 19 ++- ...{BindingViewHolder.kt => BindingHolder.kt} | 2 +- .../main/res/layout/item_follow_request.xml | 97 +++++++----- .../item_follow_request_notification.xml | 96 ------------ app/src/main/res/layout/item_hashtag.xml | 1 - app/src/main/res/layout/item_picker_list.xml | 1 - 27 files changed, 424 insertions(+), 543 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt rename app/src/main/java/com/keylesspalace/tusky/{adapter => components/compose/dialog}/AddPollOptionsAdapter.kt (62%) rename app/src/main/java/com/keylesspalace/tusky/util/{BindingViewHolder.kt => BindingHolder.kt} (82%) delete mode 100644 app/src/main/res/layout/item_follow_request_notification.xml 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 e395a7e66..fe3b15f82 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -19,60 +19,57 @@ import android.text.method.LinkMovementMethod import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup -import android.view.View -import android.widget.TextView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.item_account_field.view.* -class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter() { +class AccountFieldAdapter( + private val linkListener: LinkListener, + private val animateEmojis: Boolean +) : RecyclerView.Adapter>() { var emojis: List = emptyList() var fields: List> = emptyList() override fun getItemCount() = fields.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account_field, parent, false) - return ViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val proofOrField = fields[position] + val nameTextView = holder.binding.accountFieldName + val valueTextView = holder.binding.accountFieldValue if(proofOrField.isLeft()) { val identityProof = proofOrField.asLeft() - viewHolder.nameTextView.text = identityProof.provider - viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) + nameTextView.text = identityProof.provider + valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) - viewHolder.valueTextView.movementMethod = LinkMovementMethod.getInstance() + valueTextView.movementMethod = LinkMovementMethod.getInstance() - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { val field = proofOrField.asRight() - val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis) - viewHolder.nameTextView.text = emojifiedName + val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) + nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis) - LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) + val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) + LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener) if(field.verifiedAt != null) { - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) } } } - - class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) { - val nameTextView: TextView = rootView.accountFieldName - val valueTextView: TextView = rootView.accountFieldValue - } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index 768c28859..29cbec285 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -15,18 +15,16 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.EditText -import com.keylesspalace.tusky.R +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemEditFieldBinding import com.keylesspalace.tusky.entity.StringField -import kotlinx.android.synthetic.main.item_edit_field.view.* +import com.keylesspalace.tusky.util.BindingHolder -class AccountFieldEditAdapter : RecyclerView.Adapter() { +class AccountFieldEditAdapter : RecyclerView.Adapter>() { private val fieldData = mutableListOf() @@ -54,20 +52,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter { + val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - viewHolder.nameTextView.setText(fieldData[position].first) - viewHolder.valueTextView.setText(fieldData[position].second) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + holder.binding.accountFieldName.setText(fieldData[position].first) + holder.binding.accountFieldValue.setText(fieldData[position].second) - viewHolder.nameTextView.addTextChangedListener(object: TextWatcher { + holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher { override fun afterTextChanged(newText: Editable) { - fieldData[viewHolder.adapterPosition].first = newText.toString() + fieldData[holder.adapterPosition].first = newText.toString() } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} @@ -75,9 +73,9 @@ class AccountFieldEditAdapter : RecyclerView.Adapter(context, R.layout.item_autocomplete_account) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var view = convertView - if (convertView == null) { - val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - view = layoutInflater.inflate(R.layout.item_autocomplete_account, parent, false) + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val binding = if (convertView == null) { + ItemAutocompleteAccountBinding.inflate(LayoutInflater.from(context), parent, false) + } else { + ItemAutocompleteAccountBinding.bind(convertView) } - view!! val account = getItem(position) if (account != null) { - val username = view.username - val displayName = view.display_name - val avatar = view.avatar - val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context) + val pm = PreferenceManager.getDefaultSharedPreferences(binding.avatar.context) val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - username.text = account.fullName - displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis) + binding.username.text = account.fullName + binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis) - val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) val animateAvatar = pm.getBoolean("animateGifAvatars", false) - loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) + loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar) } - return view + return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index 70a6163db..2640caaca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -15,48 +15,44 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.BindingHolder import java.util.* -class EmojiAdapter(emojiList: List, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter() { - private val emojiList : List +class EmojiAdapter( + emojiList: List, + private val onEmojiSelectedListener: OnEmojiSelectedListener +) : RecyclerView.Adapter>() { - init { - this.emojiList = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + private val emojiList : List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + + override fun getItemCount() = emojiList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun getItemCount(): Int { - return emojiList.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji_button, parent, false) as ImageView - return EmojiHolder(view) - } - - override fun onBindViewHolder(viewHolder: EmojiHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val emoji = emojiList[position] + val emojiImageView = holder.binding.root - Glide.with(viewHolder.emojiImageView) + Glide.with(emojiImageView) .load(emoji.url) - .into(viewHolder.emojiImageView) + .into(emojiImageView) - viewHolder.emojiImageView.setOnClickListener { + emojiImageView.setOnClickListener { onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) } - viewHolder.emojiImageView.contentDescription = emoji.shortcode + emojiImageView.contentDescription = emoji.shortcode } - - class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView) - } interface OnEmojiSelectedListener { 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 8fa14731c..f19dde822 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -1,55 +1,67 @@ +/* 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.adapter import android.graphics.Typeface import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan -import android.view.View -import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener 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) { - private var id: String? = null +class FollowRequestViewHolder( + private val binding: ItemFollowRequestBinding, + private val showHeader: Boolean +) : RecyclerView.ViewHolder(binding.root) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { - id = account.id val wrappedName = account.name.unicodeWrap() val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) - itemView.displayNameTextView.text = emojifiedName + binding.displayNameTextView.text = emojifiedName if (showHeader) { val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) - itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { + binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply { setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) }.emojify(account.emojis, itemView, animateEmojis) } - itemView.notificationTextView?.visible(showHeader) + binding.notificationTextView.visible(showHeader) val format = itemView.context.getString(R.string.status_username_format) val formattedUsername = String.format(format, account.username) - itemView.usernameTextView.text = formattedUsername - val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar) + binding.usernameTextView.text = formattedUsername + val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar) } - fun setupActionListener(listener: AccountActionListener) { - itemView.acceptButton.setOnClickListener { + fun setupActionListener(listener: AccountActionListener, accountId: String) { + binding.acceptButton.setOnClickListener { val position = adapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onRespondToFollowRequest(true, id, position) + listener.onRespondToFollowRequest(true, accountId, position) } } - itemView.rejectButton.setOnClickListener { + binding.rejectButton.setOnClickListener { val position = adapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onRespondToFollowRequest(false, id, position) + listener.onRespondToFollowRequest(false, accountId, position) } } - itemView.setOnClickListener { listener.onViewAccount(id) } + itemView.setOnClickListener { listener.onViewAccount(accountId) } } } 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 9ba598842..ef14618e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.interfaces.AccountActionListener; public class FollowRequestsAdapter extends AccountAdapter { @@ -37,9 +38,8 @@ public class FollowRequestsAdapter extends AccountAdapter { switch (viewType) { default: case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_follow_request, parent, false); - return new FollowRequestViewHolder(view, false); + ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new FollowRequestViewHolder(binding, false); } case VIEW_TYPE_FOOTER: { View view = LayoutInflater.from(parent.getContext()) @@ -54,7 +54,7 @@ public class FollowRequestsAdapter extends AccountAdapter { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener); + holder.setupActionListener(accountActionListener, accountList.get(position).getId()); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt deleted file mode 100644 index c70076c83..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.keylesspalace.tusky.adapter - -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.interfaces.LinkListener - -class HashtagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val hashtag: TextView = itemView.findViewById(R.id.hashtag) - - fun setup(tag: String, listener: LinkListener) { - hashtag.text = String.format("#%s", tag) - hashtag.setOnClickListener { listener.onViewTag(tag) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt index f9b19c697..d64784274 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt @@ -21,21 +21,22 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemPickerListBinding import com.keylesspalace.tusky.entity.MastoList -import kotlinx.android.synthetic.main.item_picker_list.view.* -class ListSelectionAdapter(context: Context) : ArrayAdapter(context, R.layout.item_autocomplete_hashtag) { +class ListSelectionAdapter(context: Context) : ArrayAdapter(context, R.layout.item_picker_list) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - - val view = convertView - ?: layoutInflater.inflate(R.layout.item_picker_list, parent, false) - - getItem(position)?.let { list -> - view.title.text = list.title + val binding = if (convertView == null) { + ItemPickerListBinding.inflate(LayoutInflater.from(context), parent, false) + } else { + ItemPickerListBinding.bind(convertView) } - return view + getItem(position)?.let { list -> + binding.root.text = list.title + } + + return binding.root } } 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 e1a30759a..20140fffd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -9,7 +9,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; -import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt index 66065c7a4..b45ca95f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt @@ -16,29 +16,28 @@ package com.keylesspalace.tusky.adapter import androidx.recyclerview.widget.RecyclerView -import android.view.View import android.view.ViewGroup +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.visible -import kotlinx.android.synthetic.main.item_network_state.view.* -class NetworkStateViewHolder(itemView: View, +class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, private val retryCallback: () -> Unit) -: RecyclerView.ViewHolder(itemView) { +: RecyclerView.ViewHolder(binding.root) { fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) { - itemView.progressBar.visible(state?.status == Status.RUNNING) - itemView.retryButton.visible(state?.status == Status.FAILED) - itemView.errorMsg.visible(state?.msg != null) - itemView.errorMsg.text = state?.msg - itemView.retryButton.setOnClickListener { + binding.progressBar.visible(state?.status == Status.RUNNING) + binding.retryButton.visible(state?.status == Status.FAILED) + binding.errorMsg.visible(state?.msg != null) + binding.errorMsg.text = state?.msg + binding.retryButton.setOnClickListener { retryCallback() } if(fullScreen) { - itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT } else { - itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT } } 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 833d18f46..cdc43741f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -39,6 +39,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; @@ -125,9 +126,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { return new FollowViewHolder(view, statusDisplayOptions); } case VIEW_TYPE_FOLLOW_REQUEST: { - View view = inflater - .inflate(R.layout.item_follow_request_notification, parent, false); - return new FollowRequestViewHolder(view, true); + ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(inflater, parent, false); + return new FollowRequestViewHolder(binding, true); } case VIEW_TYPE_PLACEHOLDER: { View view = inflater @@ -233,7 +233,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (payloadForHolder == null) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); - holder.setupActionListener(accountActionListener); + holder.setupActionListener(accountActionListener, concreteNotificaton.getAccount().getId()); } } default: 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 0208b9530..fa69e3386 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -18,19 +18,18 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.CheckBox -import android.widget.RadioButton -import android.widget.TextView import androidx.emoji.text.EmojiCompat import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemPollBinding import com.keylesspalace.tusky.entity.Emoji -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.PollOptionViewData import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.calculatePercent -class PollAdapter: RecyclerView.Adapter() { +class PollAdapter: RecyclerView.Adapter>() { private var pollOptions: List = emptyList() private var voteCount: Int = 0 @@ -64,39 +63,42 @@ class PollAdapter: RecyclerView.Adapter() { } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollViewHolder { - return PollViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll, parent, false)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun getItemCount(): Int { - return pollOptions.size - } + override fun getItemCount() = pollOptions.size - override fun onBindViewHolder(holder: PollViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val option = pollOptions[position] - holder.resultTextView.visible(mode == RESULT) - holder.radioButton.visible(mode == SINGLE) - holder.checkBox.visible(mode == MULTIPLE) + val resultTextView = holder.binding.statusPollOptionResult + val radioButton = holder.binding.statusPollRadioButton + val checkBox = holder.binding.statusPollCheckbox + + resultTextView.visible(mode == RESULT) + radioButton.visible(mode == SINGLE) + checkBox.visible(mode == MULTIPLE) when(mode) { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) - val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) - .emojify(emojis, holder.resultTextView, animateEmojis) - holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) + val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context) + .emojify(emojis, resultTextView, animateEmojis) + resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 - holder.resultTextView.background.level = level - holder.resultTextView.setOnClickListener(resultClickListener) + resultTextView.background.level = level + resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis) - holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) - holder.radioButton.isChecked = option.selected - holder.radioButton.setOnClickListener { + val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis) + radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) + radioButton.isChecked = option.selected + radioButton.setOnClickListener { pollOptions.forEachIndexed { index, pollOption -> pollOption.selected = index == holder.adapterPosition notifyItemChanged(index) @@ -104,10 +106,10 @@ class PollAdapter: RecyclerView.Adapter() { } } MULTIPLE -> { - 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 -> + val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis) + checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) + checkBox.isChecked = option.selected + checkBox.setOnCheckedChangeListener { _, isChecked -> pollOptions[holder.adapterPosition].selected = isChecked } } @@ -121,13 +123,3 @@ class PollAdapter: RecyclerView.Adapter() { const val MULTIPLE = 2 } } - - - -class PollViewHolder(view: View): RecyclerView.ViewHolder(view) { - - val resultTextView: TextView = view.findViewById(R.id.status_poll_option_result) - val radioButton: RadioButton = view.findViewById(R.id.status_poll_radio_button) - val checkBox: CheckBox = view.findViewById(R.id.status_poll_checkbox) - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt index 328e96267..4206f7cfe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt @@ -63,5 +63,4 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter() { } - -class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) \ No newline at end of file +class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index b4517dc6a..e2236503d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -18,19 +18,21 @@ package com.keylesspalace.tusky.adapter import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.google.android.material.chip.Chip import com.keylesspalace.tusky.HASHTAG import com.keylesspalace.tusky.LIST import com.keylesspalace.tusky.R import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding +import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.item_tab_preference.view.* interface ItemInteractionListener { fun onTabAdded(tab: TabData) @@ -44,61 +46,69 @@ interface ItemInteractionListener { class TabAdapter(private var data: List, private val small: Boolean, private val listener: ItemInteractionListener, - private var removeButtonEnabled: Boolean = false) : RecyclerView.Adapter() { + private var removeButtonEnabled: Boolean = false +) : RecyclerView.Adapter>() { fun updateData(newData: List) { this.data = newData notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutId = if (small) { - R.layout.item_tab_preference_small + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = if (small) { + ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false) } else { - R.layout.item_tab_preference + ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false) } - val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) - return ViewHolder(view) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val context = holder.itemView.context val tab = data[position] - if (!small && tab.id == LIST) { - holder.itemView.textView.text = tab.arguments.getOrNull(1).orEmpty() - } else { - holder.itemView.textView.setText(tab.text) - } - holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + if (small) { - holder.itemView.textView.setOnClickListener { + val binding = holder.binding as ItemTabPreferenceSmallBinding + + binding.textView.setText(tab.text) + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.textView.setOnClickListener { listener.onTabAdded(tab) } - } - holder.itemView.imageView?.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - listener.onStartDrag(holder) - true + + } else { + val binding = holder.binding as ItemTabPreferenceBinding + + if (tab.id == LIST) { + binding.textView.text = tab.arguments.getOrNull(1).orEmpty() } else { - false + binding.textView.setText(tab.text) } - } - holder.itemView.removeButton?.setOnClickListener { - listener.onTabRemoved(holder.adapterPosition) - } - if (holder.itemView.removeButton != null) { - holder.itemView.removeButton.isEnabled = removeButtonEnabled + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.imageView.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + listener.onStartDrag(holder) + true + } else { + false + } + } + binding.removeButton.setOnClickListener { + listener.onTabRemoved(holder.adapterPosition) + } + binding.removeButton.isEnabled = removeButtonEnabled ThemeUtils.setDrawableTint( holder.itemView.context, - holder.itemView.removeButton.drawable, + binding.removeButton.drawable, (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) ) - } - - if (!small) { if (tab.id == HASHTAG) { - holder.itemView.chipGroup.show() + binding.chipGroup.show() /* * The chip group will always contain the actionChip (it is defined in the xml layout). @@ -107,9 +117,9 @@ class TabAdapter(private var data: List, */ tab.arguments.forEachIndexed { i, arg -> - val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? + val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? ?: Chip(context).apply { - holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1) + binding.chipGroup.addView(this, binding.chipGroup.size - 1) chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) } @@ -126,16 +136,16 @@ class TabAdapter(private var data: List, } } - while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { - holder.itemView.chipGroup.removeViewAt(tab.arguments.size) + while(binding.chipGroup.size - 1 > tab.arguments.size) { + binding.chipGroup.removeViewAt(tab.arguments.size) } - holder.itemView.actionChip.setOnClickListener { + binding.actionChip.setOnClickListener { listener.onActionChipClicked(tab, holder.adapterPosition) } } else { - holder.itemView.chipGroup.hide() + binding.chipGroup.hide() } } } @@ -148,6 +158,4 @@ class TabAdapter(private var data: List, notifyDataSetChanged() } } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) } 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 b54b15554..5014b52e2 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 @@ -19,19 +19,17 @@ import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify -import kotlinx.android.synthetic.main.item_announcement.view.* - interface AnnouncementActionListener: LinkListener { fun openReactionPicker(announcementId: String, target: View) @@ -44,16 +42,74 @@ class AnnouncementAdapter( private val listener: AnnouncementActionListener, private val wellbeingEnabled: Boolean = false, private val animateEmojis: Boolean = false -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter>() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_announcement, parent, false) - return AnnouncementViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) { - viewHolder.bind(items[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val item = items[position] + + val text = holder.binding.text + val chips = holder.binding.chipGroup + val addReactionChip = holder.binding.addReactionChip + + LinkHelper.setClickableText(text, item.content, null, listener) + + // If wellbeing mode is enabled, announcement badge counts should not be shown. + if (wellbeingEnabled) { + // Since reactions are not visible in wellbeing mode, + // we shouldn't be able to add any ourselves. + addReactionChip.visibility = View.GONE + return + } + + item.reactions.forEachIndexed { i, reaction -> + (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? + ?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { + isCheckable = true + checkedIcon = null + chips.addView(this, i) + }) + .apply { + val emojiText = if (reaction.url == null) { + reaction.name + } else { + context.getString(R.string.emoji_shortcode_format, reaction.name) + } + this.text = ("$emojiText ${reaction.count}") + .emojify( + listOf(Emoji( + reaction.name, + reaction.url ?: "", + reaction.staticUrl ?: "", + null + )), + this, + animateEmojis + ) + + isChecked = reaction.me + + setOnClickListener { + if (reaction.me) { + listener.removeReaction(item.id, reaction.name) + } else { + listener.addReaction(item.id, reaction.name) + } + } + } + } + + while (chips.size - 1 > item.reactions.size) { + chips.removeViewAt(item.reactions.size) + } + + addReactionChip.setOnClickListener { + listener.openReactionPicker(item.id, it) + } } override fun getItemCount() = items.size @@ -62,67 +118,4 @@ class AnnouncementAdapter( this.items = items notifyDataSetChanged() } - - inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { - private val text: TextView = view.text - private val chips: ChipGroup = view.chipGroup - private val addReactionChip: Chip = view.addReactionChip - - fun bind(item: Announcement) { - LinkHelper.setClickableText(text, item.content, null, listener) - - // If wellbeing mode is enabled, announcement badge counts should not be shown. - if (wellbeingEnabled) { - // Since reactions are not visible in wellbeing mode, - // we shouldn't be able to add any ourselves. - addReactionChip.visibility = View.GONE - return - } - - item.reactions.forEachIndexed { i, reaction -> - (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? - ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { - isCheckable = true - checkedIcon = null - chips.addView(this, i) - }) - .apply { - val emojiText = if (reaction.url == null) { - reaction.name - } else { - view.context.getString(R.string.emoji_shortcode_format, reaction.name) - } - text = ("$emojiText ${reaction.count}") - .emojify( - listOf(Emoji( - reaction.name, - reaction.url ?: "", - reaction.staticUrl ?: "", - null - )), - this, - animateEmojis - ) - - isChecked = reaction.me - - setOnClickListener { - if (reaction.me) { - listener.removeReaction(item.id, reaction.name) - } else { - listener.addReaction(item.id, reaction.name) - } - } - } - } - - while (chips.size - 1 > item.reactions.size) { - chips.removeViewAt(item.reactions.size) - } - - addReactionChip.setOnClickListener { - listener.openReactionPicker(item.id, it) - } - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index 09da54626..6ace77bc3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -22,7 +22,6 @@ import android.view.LayoutInflater import android.view.WindowManager import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter import com.keylesspalace.tusky.databinding.DialogAddPollBinding import com.keylesspalace.tusky.entity.NewPoll diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt similarity index 62% rename from app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt rename to app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt index 602419925..6a0b6a871 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt @@ -13,17 +13,16 @@ * 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.dialog import android.text.InputFilter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAddPollOptionBinding +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.visible @@ -32,7 +31,7 @@ class AddPollOptionsAdapter( private val maxOptionLength: Int, private val onOptionRemoved: (Boolean) -> Unit, private val onOptionChanged: (Boolean) -> Unit -): RecyclerView.Adapter() { +): RecyclerView.Adapter>() { val pollOptions: List get() = options.toList() @@ -42,11 +41,12 @@ class AddPollOptionsAdapter( notifyItemInserted(options.size - 1) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val holder = ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_add_poll_option, parent, false)) - holder.editText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) + binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) - holder.editText.onTextChanged { s, _, _, _ -> + binding.optionEditText.onTextChanged { s, _, _, _ -> val pos = holder.adapterPosition if(pos != RecyclerView.NO_POSITION) { options[pos] = s.toString() @@ -59,15 +59,15 @@ class AddPollOptionsAdapter( override fun getItemCount() = options.size - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.editText.setText(options[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + holder.binding.optionEditText.setText(options[position]) - holder.textInputLayout.hint = holder.textInputLayout.context.getString(R.string.poll_new_choice_hint, position + 1) + holder.binding.optionTextInputLayout.hint = holder.binding.root.context.getString(R.string.poll_new_choice_hint, position + 1) - holder.deleteButton.visible(position > 1, View.INVISIBLE) + holder.binding.deleteButton.visible(position > 1, View.INVISIBLE) - holder.deleteButton.setOnClickListener { - holder.editText.clearFocus() + holder.binding.deleteButton.setOnClickListener { + holder.binding.optionEditText.clearFocus() options.removeAt(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition) onOptionRemoved(validateInput()) @@ -81,12 +81,4 @@ class AddPollOptionsAdapter( return true } - } - - -class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { - val textInputLayout: TextInputLayout = itemView.findViewById(R.id.optionTextInputLayout) - val editText: TextInputEditText = itemView.findViewById(R.id.optionEditText) - val deleteButton: ImageButton = itemView.findViewById(R.id.deleteButton) -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 6d6aee481..376d3cd56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -1,3 +1,18 @@ +/* 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.conversation import android.view.LayoutInflater @@ -10,6 +25,7 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.NetworkStateViewHolder +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -49,11 +65,15 @@ class ConversationAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { - R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback) - R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions, - listener) + R.layout.item_network_state -> { + val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) + NetworkStateViewHolder(binding, retryCallback) + } + R.layout.item_conversation -> { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + ConversationViewHolder(view, statusDisplayOptions, listener) + } else -> throw IllegalArgumentException("unknown view type $viewType") } } 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 index 5dfbceac8..7fd224aca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -23,7 +23,7 @@ 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.BindingHolder import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.visible @@ -35,7 +35,7 @@ interface DraftActionListener { class DraftsAdapter( private val listener: DraftActionListener -) : PagedListAdapter>( +) : PagedListAdapter>( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { return oldItem.id == newItem.id @@ -47,11 +47,11 @@ class DraftsAdapter( } ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) - val viewHolder = BindingViewHolder(binding) + val viewHolder = BindingHolder(binding) binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) binding.draftMediaPreview.adapter = DraftMediaAdapter { @@ -63,7 +63,7 @@ class DraftsAdapter( return viewHolder } - override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { getItem(position)?.let { draft -> holder.binding.root.setOnClickListener { listener.onOpenDraft(draft) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt index 62ab7ef31..de699d126 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -1,22 +1,31 @@ package com.keylesspalace.tusky.components.instancemute.adapter import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener -import kotlinx.android.synthetic.main.item_muted_domain.view.* +import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding +import com.keylesspalace.tusky.util.BindingHolder + +class DomainMutesAdapter( + private val actionListener: InstanceActionListener +): RecyclerView.Adapter>() { -class DomainMutesAdapter(private val actionListener: InstanceActionListener): RecyclerView.Adapter() { var instances: MutableList = mutableListOf() var bottomLoading: Boolean = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_muted_domain, parent, false), actionListener) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemMutedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.setupWithInstance(instances[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val instance = instances[position] + + holder.binding.mutedDomain.text = instance + holder.binding.mutedDomainUnmute.setOnClickListener { + actionListener.mute(false, instance, holder.adapterPosition) + } } override fun getItemCount(): Int { @@ -37,21 +46,10 @@ class DomainMutesAdapter(private val actionListener: InstanceActionListener): Re notifyItemInserted(instances.size) } - fun removeItem(position: Int) - { + fun removeItem(position: Int) { if (position >= 0 && position < instances.size) { instances.removeAt(position) notifyItemRemoved(position) } } - - - class ViewHolder(rootView: View, private val actionListener: InstanceActionListener): RecyclerView.ViewHolder(rootView) { - fun setupWithInstance(instance: String) { - itemView.muted_domain.text = instance - itemView.muted_domain_unmute.setOnClickListener { - actionListener.mute(false, instance, adapterPosition) - } - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index ea12d1ffa..414130ddb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -18,13 +18,11 @@ package com.keylesspalace.tusky.components.scheduled import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.TextView import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.util.BindingHolder interface ScheduledTootActionListener { fun edit(item: ScheduledStatus) @@ -33,7 +31,7 @@ interface ScheduledTootActionListener { class ScheduledTootAdapter( val listener: ScheduledTootActionListener -) : PagedListAdapter( +) : PagedListAdapter>( object: DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { return oldItem.id == newItem.id @@ -46,40 +44,24 @@ class ScheduledTootAdapter( } ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_scheduled_toot, parent, false) - return TootViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) { - getItem(position)?.let{ - viewHolder.bind(it) - } - } - - - inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - private val text: TextView = view.findViewById(R.id.text) - private val edit: ImageButton = view.findViewById(R.id.edit) - private val delete: ImageButton = view.findViewById(R.id.delete) - - fun bind(item: ScheduledStatus) { - edit.isEnabled = true - delete.isEnabled = true - text.text = item.params.text - edit.setOnClickListener { v: View -> + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + getItem(position)?.let{ item -> + holder.binding.edit.isEnabled = true + holder.binding.delete.isEnabled = true + holder.binding.text.text = item.params.text + holder.binding.edit.setOnClickListener { v: View -> v.isEnabled = false listener.edit(item) } - delete.setOnClickListener { v: View -> + holder.binding.delete.setOnClickListener { v: View -> v.isEnabled = false listener.delete(item) } - } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt index 71863d430..ebc021602 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -19,24 +19,23 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.HashtagViewHolder +import com.keylesspalace.tusky.databinding.ItemHashtagBinding import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder class SearchHashtagsAdapter(private val linkListener: LinkListener) - : PagedListAdapter(HASHTAG_COMPARATOR) { + : PagedListAdapter>(HASHTAG_COMPARATOR) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_hashtag, parent, false) - return HashtagViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { getItem(position)?.let { (name) -> - (holder as HashtagViewHolder).setup(name, linkListener) + holder.binding.root.text = String.format("#%s", name) + holder.binding.root.setOnClickListener { linkListener.onViewTag(name) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt similarity index 82% rename from app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt rename to app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt index 14aee81b5..a7a4c9720 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt @@ -3,6 +3,6 @@ package com.keylesspalace.tusky.util import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -class BindingViewHolder( +class BindingHolder( val binding: T ) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/res/layout/item_follow_request.xml b/app/src/main/res/layout/item_follow_request.xml index 36e442c5c..e970e1289 100644 --- a/app/src/main/res/layout/item_follow_request.xml +++ b/app/src/main/res/layout/item_follow_request.xml @@ -1,52 +1,69 @@ - + android:paddingRight="16dp" + android:paddingBottom="10dp"> + + + android:layout_marginTop="10dp" + android:contentDescription="@string/action_view_profile" + tools:src="@drawable/avatar_default" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/notificationTextView" /> - + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintStart_toEndOf="@id/avatar" + app:layout_constraintTop_toTopOf="@id/avatar" + app:layout_constraintBottom_toTopOf="@id/usernameTextView" + tools:text="Display name" /> - - - - - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_follow_request_notification.xml b/app/src/main/res/layout/item_follow_request_notification.xml deleted file mode 100644 index d4db2a3dd..000000000 --- a/app/src/main/res/layout/item_follow_request_notification.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_hashtag.xml b/app/src/main/res/layout/item_hashtag.xml index a158240c9..efdb24f72 100644 --- a/app/src/main/res/layout/item_hashtag.xml +++ b/app/src/main/res/layout/item_hashtag.xml @@ -1,6 +1,5 @@ Date: Sun, 7 Mar 2021 19:29:45 +0100 Subject: [PATCH 13/41] fix MainActivity switching to notification tab when opened from recents (#2099) --- app/src/main/java/com/keylesspalace/tusky/MainActivity.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 7cbbf0c7d..d46d3d560 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -175,8 +175,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }) } - } else if (accountRequested) { - // user clicked a notification, show notification tab and switch user if necessary + } else if (accountRequested && savedInstanceState == null) { + // user clicked a notification, show notification tab showNotificationTab = true } } @@ -686,7 +686,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje glide.asDrawable() .load(avatarUrl) .transform( - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) ) .apply { if (showPlaceholder) { @@ -700,6 +700,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } + override fun onResourceReady(resource: Drawable, transition: Transition?) { binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) } From 0b86f581390912e06fb05bb90bb25e2b41db1964 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Mar 2021 19:43:25 +0100 Subject: [PATCH 14/41] convert some regular strings to plural strings (#2100) * convert some regular strings to plural strings * convert hint_describe_for_visually_impaired from string to plurals * fix ukrainian strings --- .../tusky/TabPreferenceActivity.kt | 2 +- .../components/compose/ComposeActivity.kt | 2 +- .../compose/dialog/CaptionDialog.kt | 4 ++-- .../notifications/NotificationHelper.java | 2 +- app/src/main/res/values-ar/strings.xml | 14 +++++++++---- app/src/main/res/values-bg/strings.xml | 18 +++++++++++----- app/src/main/res/values-bn-rBD/strings.xml | 10 ++++++--- app/src/main/res/values-bn-rIN/strings.xml | 14 +++++++++---- app/src/main/res/values-ca/strings.xml | 18 +++++++++++----- app/src/main/res/values-ckb/strings.xml | 18 +++++++++++----- app/src/main/res/values-cs/strings.xml | 12 ++++++++--- app/src/main/res/values-cy/strings.xml | 4 +++- app/src/main/res/values-de/strings.xml | 19 +++++++++++++---- app/src/main/res/values-eo/strings.xml | 12 ++++++++--- app/src/main/res/values-es/strings.xml | 16 ++++++++++---- app/src/main/res/values-eu/strings.xml | 12 ++++++++--- app/src/main/res/values-fa/strings.xml | 14 +++++++++---- app/src/main/res/values-fr/strings.xml | 18 +++++++++++----- app/src/main/res/values-ga/strings.xml | 14 +++++++++---- app/src/main/res/values-gd/strings.xml | 18 +++++++++++----- app/src/main/res/values-gl/strings.xml | 18 +++++++++++----- app/src/main/res/values-hi/strings.xml | 10 ++++++--- app/src/main/res/values-hu/strings.xml | 18 +++++++++++----- app/src/main/res/values-is/strings.xml | 18 +++++++++++----- app/src/main/res/values-it/strings.xml | 16 ++++++++++---- app/src/main/res/values-ja/strings.xml | 12 ++++++++--- app/src/main/res/values-ko/strings.xml | 14 +++++++++---- app/src/main/res/values-nl/strings.xml | 12 ++++++++--- app/src/main/res/values-no-rNB/strings.xml | 18 +++++++++++----- app/src/main/res/values-oc/strings.xml | 14 +++++++++---- app/src/main/res/values-pl/strings.xml | 14 +++++++++---- app/src/main/res/values-pt-rBR/strings.xml | 18 +++++++++++----- app/src/main/res/values-ru/strings.xml | 12 ++++++++--- app/src/main/res/values-sa/strings.xml | 14 +++++++++---- app/src/main/res/values-sl/strings.xml | 14 +++++++++---- app/src/main/res/values-sv/strings.xml | 12 ++++++++--- app/src/main/res/values-ta/strings.xml | 4 +++- app/src/main/res/values-th/strings.xml | 18 +++++++++++----- app/src/main/res/values-tr/strings.xml | 14 +++++++++---- app/src/main/res/values-uk/strings.xml | 18 +++++++++++----- app/src/main/res/values-vi/strings.xml | 18 +++++++++++----- app/src/main/res/values-zh-rCN/strings.xml | 21 ++++++++++++------- app/src/main/res/values-zh-rHK/strings.xml | 19 +++++++++++------ app/src/main/res/values-zh-rMO/strings.xml | 12 ++++++++--- app/src/main/res/values-zh-rSG/strings.xml | 12 ++++++++--- app/src/main/res/values-zh-rTW/strings.xml | 19 +++++++++++------ app/src/main/res/values/strings.xml | 20 +++++++++++++----- 47 files changed, 469 insertions(+), 181 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 2fd599026..413e754bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -144,7 +144,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene toggleFab(false) } - binding.maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) + binding.maxTabsInfo.text = resources.getQuantityString(R.plurals.max_tab_number_reached, MAX_TAB_COUNT, MAX_TAB_COUNT) updateAvailableTabs() } 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 26e1d5d97..5c5b35328 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 @@ -839,7 +839,7 @@ class ComposeActivity : BaseActivity(), val count = clipData.itemCount 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() + Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() } else { // if not grater then 4, upload all multiple media. for (i in 0 until count) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 17db6ff6f..50d5c23b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -63,8 +63,8 @@ fun T.makeCaptionDialog(existingDescription: String?, (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) val input = EditText(this) - input.hint = getString(R.string.hint_describe_for_visually_impaired, - MEDIA_DESCRIPTION_CHARACTER_LIMIT) + input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT) dialogLayout.addView(input) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) input.setLines(2) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 87a5d261c..84a07490f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -242,7 +242,7 @@ public class NotificationHelper { if (currentNotifications.length() != 1) { try { - String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); + String title = context.getResources().getQuantityString(R.plurals.notification_title_summary, currentNotifications.length(), currentNotifications.length()); String text = joinNames(context, currentNotifications); summaryBuilder.setContentTitle(title) .setContentText(text); diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6f4b1cee7..c2098f070 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -214,7 +214,9 @@ %1$s, %2$s, %3$s و %4$d آخرون %1$s, %2$s, و %3$s %1$s و %2$s - %d تفاعلات جديدة + + %d تفاعلات جديدة + حساب مقفل عن التطبيق توسكي %s @@ -275,8 +277,10 @@ إزالة الحساب مِن القائمة النشر بواسطة حساب %1$s تعذرت عملية إضافة الشرح - وصف لضعاف البصر -\n(%d أحرف على أقصى تقدير) + + وصف لضعاف البصر +\n(%d أحرف على أقصى تقدير) + إضافة شرح حذف تجميد الحساب @@ -325,7 +329,9 @@ %1$s %1$s و %2$s %1$s و %2$s و %3$d آخَرون - لقد بلغت الحد الأقصى مِن الألسنة %1$d + + لقد بلغت الحد الأقصى مِن الألسنة %1$d + الوسائط: %s تحذير عن المحتوى: %s مِن دون وصف diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 61c97505f..0056bfdb7 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -76,7 +76,9 @@ Без описание Предупреждение за съдържание: %s Мултимедия: %s - достигнати са максималните %1$d раздела + + достигнати са максималните %1$d раздела + %1$s, %2$s и %3$d други %1$s %1$s и %2$s @@ -134,8 +136,10 @@ Заключване на акаунт Премахване Задаване на надпис - Опишете за хора със зрителни увреждания -\n(%d ограничение на знаците) + + Опишете за хора със зрителни увреждания +\n(%d ограничение на знаците) + Неуспешно задаване на надпис Публикуване с акаунт %1$s Премахване на акаунт от списъка @@ -195,7 +199,9 @@ Tusky %s Относно Заключен акаунт - %d нови взаимодействия + + %d нови взаимодействия + %1$s и %2$s %1$s, %2$s, и %3$s %1$s, %2$s, %3$s и %4$d други @@ -441,7 +447,9 @@ \n Все още можете да осъществите достъп до старите си чернови чрез бутон на екрана за нови чернови, но те ще бъдат премахнати при бъдеща актуализация! Тази публикация не успя да се изпрати! Наистина ли искате да изтриете списъка %s\? - Не можете да качите повече от %1$d мултимедийни прикачени файлове. + + Не можете да качите повече от %1$d мултимедийни прикачени файлове. + Скриване на количествена статистика на профили Скриване на количествена статистика на публикации Ограничаване на известия от емисия diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 72fb77e40..c547ee143 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -12,7 +12,9 @@ বর্ণনা নাই সতর্কবার্তা: %s মিডিয়া: %s - সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + + সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + দ্বারা পছন্দ দ্বারা সর্মথন পিন @@ -57,8 +59,10 @@ অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন অ্যাকাউন্ট লক করুন ক্যাপশন সেট করুন - দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন -\n(%d অক্ষর সীমা) + + দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%d অক্ষর সীমা) + ক্যাপশন সেট করতে ব্যর্থ অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে তালিকা থেকে অ্যাকাউন্ট সরান diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index ae3ae2f21..c13a880de 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -225,7 +225,9 @@ %1$s,%2$s,%3$s এবং %4$d আরো অন্য জন %1$s, %2$s, আর %3$s %1$s আর %2$s - %d নতুন মিথস্ক্রিয়া + + %d নতুন মিথস্ক্রিয়া + লক অ্যাকাউন্ট সম্পর্কিত টাস্কি %s @@ -286,8 +288,10 @@ তালিকা থেকে অ্যাকাউন্ট সরান অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে ক্যাপশন সেট করতে ব্যর্থ - দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন -\n(%d অক্ষর সীমা) + + দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%d অক্ষর সীমা) + ক্যাপশন সেট করুন সরান অ্যাকাউন্ট লক করুন @@ -345,7 +349,9 @@ %1$s %1$s এবং %2$s %1$s, %2$s এবং %3$d আরো অন্য জন - সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + + সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + মিডিয়া: %s সতর্কবার্তা: %s বর্ণনা নাই diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index daecc343b..8dbaf1218 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -153,7 +153,9 @@ %1$s, %2$s, %3$s i %4$d més %1$s, %2$s i %3$s %1$s i %2$s - %d interaccions noves + + %d interaccions noves + Compte blocat Quant a Tusky %s @@ -286,8 +288,10 @@ Suprimir un compte de la llista Publicar amb el compte %1$s Error al afegir la llegenda - Descriure per a invidentes -\n(%d character limit) + + Descriure per a invidentes +\n(%d character limit) + Afegir una llegenda Eliminar Protegir el compte @@ -342,7 +346,9 @@ %1$s %1$s i %2$s %1$s, %2$s i %3$d més - màxim de %1$d pestanyes aconseguides + + màxim de %1$d pestanyes aconseguides + Mèdia : %s Sense descripció Favorits @@ -493,7 +499,9 @@ 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. + + 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 diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 56f4f5e95..5fb07bed5 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -234,7 +234,9 @@ %d کاژێرماوە کاتێک وشەکە یان دەستەواژەکە تەنها ئەبجەدییە، تەنها ئەگەر لەگەڵ هەموو وشەکە یەکبێت کاری پێدەکرێت - ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + + ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + شاردنەوەی زانیاری چەندێتی لەسەر پرۆفایلەکان شاردنەوەی زانیاری چەندێتی لە بابەتەکان سنووردارکردنی ئاگانامەکانی تایم لاین @@ -327,7 +329,9 @@ هیچ وەسفێک ئاگاداری ناوەڕۆک: %s میدیا: %s - بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + + بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + %1$s, %2$s و %3$d زیاتر %1$s و %2$s %1$s @@ -384,8 +388,10 @@ داخستنی ئەژمێر لابردن دانانی سەردێڕ - وەسف بکە بۆ بینایی داڕماو -\n(%d سنوری کاراکتەر) + + وەسف بکە بۆ بینایی داڕماو +\n(%d سنوری کاراکتەر) + دانانی سەردێڕ شکستی هێنا بڵاوکردنەوە بە هەژماری %1$s لابردنی ئەژمێر لە لیستەکە @@ -442,7 +448,9 @@ توسکی %s سەبارەت هەژماری داخراو - %d چالاکی نوێ + + %d چالاکی نوێ + %1$s و %2$s %1$s و %2$s و %3$s %1$s, %2$s, %3$s و %4$d ئەوانی تر diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index bac6fe1a6..558db2ecb 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s a dalších %4$d %1$s, %2$s a %3$s %1$s a %2$s - %d nových interakcí + + %d nových interakcí + Uzamčený účet O této aplikaci Tusky %s @@ -285,7 +287,9 @@ Odstranit účet ze seznamu Píšete s účtem %1$s Nastavení popisku selhalo - Popis pro zrakově postižené\n(limit %d znaků) + + Popis pro zrakově postižené\n(limit %d znaků) + Nastavit popisek Odstranit Uzamknout účet @@ -344,7 +348,9 @@ %1$s %1$s a %2$s %1$s, %2$s a %3$d další - bylo dosaženo maxima %1$d panelů + + bylo dosaženo maxima %1$d panelů + Média %s Varování o obsahu: %s diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index b41bbd6f9..c5a8af44b 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -190,7 +190,9 @@ %1$s, %2$s, %3$s a %4$d eraill %1$s, %2$s, a %3$s %1$s a %2$s - %d rhyngweithiad newydd + + %d rhyngweithiad newydd + Cyfrif wedi\'i gloi Amdano Mae Tusky yn feddalwedd ffynhonnell agored barn rydd. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2a1d8a144..bae3e4e30 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -217,7 +217,10 @@ %1$s, %2$s, %3$s und %4$d andere %1$s, %2$s, und %3$s %1$s und %2$s - %d neue Interaktionen + + %d neue Interaktion + %d neue Interaktionen + Gesperrtes Profil Über Tusky ist freie und quelloffene Software. Es ist lizenziert unter der GNU General Public License Version 3. Du kannst dir die Lizenz hier anschauen: https://www.gnu.org/licenses/gpl-3.0.de.html @@ -261,7 +264,9 @@ Ein Konto zu einer Liste hinzufügen verfassen mit %1$s Fehler beim Speichern der Beschreibung - Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen) + + Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen) + Beschreibung eingeben Entfernen Gesperrtes Profil @@ -310,7 +315,10 @@ %1$s %1$s und %2$s %1$s, %2$s und %3$d mehr - Maximum von %1$d Tabs erreicht + + Maximum von %1$d Tab erreicht + Maximum von %1$d Tabs erreicht + Keine Beschreibung Favorisiert Öffentlich @@ -477,7 +485,10 @@ \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. + + Du kannst nicht mehr als %1$d Anhang hochladen. + Du kannst nicht mehr als %1$d Anhänge hochladen. + Wohlbefinden Dauer Für immer diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 34b5a5bc3..b55353d49 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -216,7 +216,9 @@ %1$s, %2$s, %3$s kaj %4$d aliaj %1$s, %2$s, kaj %3$s %1$s kaj %2$s - %d novaj interagoj + + %d novaj interagoj + Ŝlosita konto Pri Tusky %s @@ -281,7 +283,9 @@ Forigi konton el la listo Afiŝi per konto %1$s Redakto de apudskribo malsukcesis - Priskribi por misvidantaj homoj\n(%d signoj maksimume) + + Priskribi por misvidantaj homoj\n(%d signoj maksimume) + Redakti apudskribon Forigi Ŝlosi konton @@ -338,7 +342,9 @@ %1$s %1$s kaj %2$s %1$s, %2$s kaj %3$d aliaj - maksimuma nombro %1$d da langetoj atingita + + maksimuma nombro %1$d da langetoj atingita + Aŭdovidaĵo: %s Enhava averto: %s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c96229a4a..127a8ed02 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -203,7 +203,9 @@ %1$s, %2$s, %3$s y %4$d otros %1$s, %2$s, y %3$s %1$s y %2$s - %d nuevas interacciones + + %d nuevas interacciones + Cuenta protegida Acerca de Tusky %s @@ -251,7 +253,9 @@ Cronología de lista Publicando con la cuenta %1$s Error al añadir leyenda - Describir para invidentes\n(límite de %d caracteres) + + Describir para invidentes\n(límite de %d caracteres) + Añadir leyenda Eliminar Proteger cuenta @@ -308,7 +312,9 @@ %1$s %1$s y %2$s %1$s, %2$s y %3$d más - máximo de %1$d pestañas alcanzadas + + máximo de %1$d pestañas alcanzadas + Menciones Mostrar favoritos Menciones @@ -480,7 +486,9 @@ No hay anuncios. Anuncios %s recién publicado - No puedes cargar más de %1$d archivos adjuntos multimedia. + + 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 diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 0c6169582..15925a361 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -192,7 +192,9 @@ %1$s, %2$s, %3$s eta beste %4$d %1$s, %2$s eta %3$s %1$s eta %2$s - %d interakzio berri + + %d interakzio berri + Kontu babestua Honi buruz Tusky software libre eta kode askekoa da. @@ -235,7 +237,9 @@ Zerrenda denbora-lerroa %1$s kontuarekin tut egiten Akatsa deskribapena eranstean - Ikusmen urritasuna dutenentzat deskribapena\n(%d karaktereko muga) + + Ikusmen urritasuna dutenentzat deskribapena\n(%d karaktereko muga) + Deskribapena erantsi Ezabatu Kontua babestu @@ -361,7 +365,9 @@ %1$s %1$s eta %2$s %1$s, %2$s eta %3$d gehiago - gehienezko %1$d fitxa iritsita + + gehienezko %1$d fitxa iritsita + Media: %s Edukiaren abisua: %s Deskribapenik ez diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2d5f570e4..82e77ff96 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -192,7 +192,9 @@ %1$s، %2$s، %3$s و %4$d دیگر %1$s، %2$s و %3$s %1$s و %2$s - %d برهم‌کنش جدید + + %d برهم‌کنش جدید + حساب قفل‌شده درباره تاسکی نرم‌افزاری آزاد است که تحت نگارش ۳ از پروانهٔ جامع همگانی گنو منتشر شده است. پروانه را می‌توانید از این‌جا ببینید: https://www.gnu.org/licenses/gpl-3.0.en.html @@ -230,8 +232,10 @@ خط زمانی فهرست در حال فرستادن با حساب %1$s شکست در تنظیم عنوان - توصیف برای کم‌بینایان -\n(کران %d نویسه) + + توصیف برای کم‌بینایان +\n(کران %d نویسه) + تنظیم عنوان برداشتن قفل حساب @@ -352,7 +356,9 @@ %1$s %1$s و %2$s %1$s، %2$s و %3$d بیش‌تر - رسیده به بیشینهٔ %1$d زبانه + + رسیده به بیشینهٔ %1$d زبانه + رسانه: %s هشدار محتوا: %s بدون هیچ توضیحی diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 74f5e1055..41d01a443 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s et %4$d autres %1$s, %2$s et %3$s %1$s et %2$s - %d nouvelles interactions + + %d nouvelles interactions + Compte verrouillé À propos Tusky %s @@ -285,8 +287,10 @@ Supprimer un compte de la liste Publier avec le compte %1$s Impossible de définir la légende - Décrire pour les malvoyants -\n(%d caractères maximum) + + Décrire pour les malvoyants +\n(%d caractères maximum) + Mettre une légende Supprimer le média Verrouiller le compte @@ -343,7 +347,9 @@ %1$s %1$s et %2$s %1$s, %2$s et %3$d autres - nombre maximum d\'onglets %1$d atteint + + nombre maximum d\'onglets %1$d atteint + Média : %s Avertissement : %s @@ -499,7 +505,9 @@ %s vient de publier Examiner les notifications Nouveau pouets - Vous ne pouvez pas téléverser plus de %1$d pièces jointes. + + 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 diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 130f4abef..18261a959 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -190,7 +190,9 @@ Cumhachtaithe ag Tusky Tusky %s Cuntas faoi Ghlas - %d idirghníomhaíochtaí nua + + %d idirghníomhaíochtaí nua + %1$s agus %2$s %1$s, %2$s, agus %3$s %1$s, %2$s, %3$s agus %4$d cinn eile @@ -290,7 +292,9 @@ Sraith emoji reatha Google Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Tusky: Féadfaidh an fhaisnéis thíos próifíl an úsáideora a léiriú go neamhiomlán. Brúigh chun próifíl iomlán a oscailt sa bhrabhsálaí. - uasmhéid de chluaisíní %1$d sroichte + + uasmhéid de chluaisíní %1$d sroichte + Vótaíocht le roghanna: %1$s, %2$s, %3$s, %4$s; %5$s Liosta Cumadh Tút @@ -319,8 +323,10 @@ Bain cuntas ón liosta Postáil le cuntas %1$s Theip ar an bhfotheideal a shocrú - Déan cur síos ar dhaoine lagamhairc -\n(teorainn carachtar %d) + + Déan cur síos ar dhaoine lagamhairc +\n(teorainn carachtar %d) + Socraigh fotheideal Bain Cuntas glasála diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 44d1a1d76..8e49477a0 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -74,7 +74,9 @@ 5 mionaidean Gun chrìoch Faide - Chan urrainn dhut barrachd air %1$d ceanglacha(i)n meadhain a luchdadh suas. + + Chan urrainn dhut barrachd air %1$d ceanglacha(i)n meadhain a luchdadh suas. + Falaich an stadastaireachd àireamhail air pròifilean Falaich an stadastaireachd àireamhail air postaichean Cuingich na brathan mun loidhne-ama @@ -184,7 +186,9 @@ Gun tuairisgeul Rabhadh susbainte: %s Meadhan: %s - ràinig thu na tha ceadaichte dhe %1$d taba(ichean) + + ràinig thu na tha ceadaichte dhe %1$d taba(ichean) + %1$s, %2$s ’s %3$d eile %1$s ’Na annsachd aig @@ -243,8 +247,10 @@ Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas Suidhidh am fo-thiotal - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) + + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn +\n(%d caractar(an) air a char as fhaide) + Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s Thoir an cunntas air falbh on liosta @@ -325,7 +331,9 @@ Le cumhachd Tusky Tusky %s Cunntas glaiste - %d eadar-ghabhail(ean) ùr(a) + + %d eadar-ghabhail(ean) ùr(a) + %1$s ’s %2$s %1$s, %2$s ’s %3$s %1$s, %2$s, %3$s ’s %4$d eile diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 75dd1c565..504e1010f 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -200,7 +200,9 @@ Sen descrición Aviso sobre o contido: %s Multimedia: %s - acadouse o máximo de %1$d lapelas + + acadouse o máximo de %1$d lapelas + %1$s, %2$s e %3$d máis %1$s e %2$s Favorecido por @@ -257,8 +259,10 @@ Bloquear conta Eliminar Escribir descrición - Describe para persoas con problemas de visión -\n(%d caracteres como máximo) + + Describe para persoas con problemas de visión +\n(%d caracteres como máximo) + Fallou establecemento do texto Publicar coa conta %1$s Eliminar conta da listaxe @@ -318,7 +322,9 @@ Tusky %s Acerca de Conta bloqueada - %d novas interaccións + + %d novas interaccións + %1$s e %2$s %1$s, %2$s, e %3$s %1$s, %2$s, %3$s e %4$d outras @@ -435,7 +441,9 @@ \nAínda podes acceder aos antigos borradores a través do botón na pantalla de novos borradores, pero eliminarémolo en futuras actualizacións! Fallou o envío do toot! Tes a certeza de querer eliminar a listaxe %s\? - Non podes subir máis de %1$d anexos multimedia. + + Non podes subir máis de %1$d anexos multimedia. + Agochar estatísticas cuantitativas nos perfís Agochar estatísticas cuantitativas nas publicacións Limitar notificacións da cronoloxía diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 3d4fe2c33..dd1a60e86 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -280,7 +280,9 @@ असूचीबद्ध सार्वजनिक विषय वस्तु चेतावनी: %s - अधिकतम %1$d टैब तक पहुंच गऐ + + अधिकतम %1$d टैब तक पहुंच गऐ + %1$s, %2$s तथा %3$d अन्य लोग %1$s तथा %2$s %1$s @@ -310,8 +312,10 @@ टूट भेजने में त्रुटि हटाएँ हटा दें - दृष्टिहीन लोगों के लिए वर्णन करें -\n(%d वर्ण सीमा) + + दृष्टिहीन लोगों के लिए वर्णन करें +\n(%d वर्ण सीमा) + कैप्शन सेट करने में विफल %1$s खाते के साथ पोस्ट कर रहे सूची से खाता निकालें diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 0cc2f2763..dbb5c8663 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -205,7 +205,9 @@ %1$s, %2$s, %3$s és még %4$d %1$s, %2$s meg %3$s %1$s és %2$s - %d új interakció + + %d új interakció + Zárolt fiók Rólunk Tusky %s @@ -273,7 +275,9 @@ Megtolta Kedvencnek jelölte %1$s és %2$s - elérted a fülek maximális számát (%1$d) + + elérted a fülek maximális számát (%1$d) + Nincs leírás Nyilvános @@ -338,8 +342,10 @@ Fiók eltávolítása a listából Tülkölés %1$s fiókkal Cím beállítása nem sikerült - Leírás látássérülteknek -\n(%d karakter korlát) + + Leírás látássérülteknek +\n(%d karakter korlát) + Cím beállítása Minden követődet külön engedélyezned kell Minden tülk kibontása/összecsukása @@ -484,7 +490,9 @@ \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. + + 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 diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 8415b61af..d73446e2f 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -243,7 +243,9 @@ %1$s, %2$s, %3$s og %4$d til viðbótar %1$s, %2$s og %3$s %1$s og %2$s - %d nýjar aðgerðir + + %d nýjar aðgerðir + Læstur notandaaðgangur Tusky %s Keyrir á Tusky @@ -298,8 +300,10 @@ Fjarlægja notandaaðganginn af listanum Sendi með notandaaðgangnum %1$s Ekki tókst að setja skýringatexta - Lýstu þessu fyrir sjónskerta -\n(hámark %d stafir) + + Lýstu þessu fyrir sjónskerta +\n(hámark %d stafir) + Setja skýringatexta Fjarlægja Læsa notandaaðgangi @@ -349,7 +353,9 @@ %1$s %1$s og %2$s %1$s, %2$s og %3$d til viðbótar - hámarksfjölda %1$d flipa náð + + hámarksfjölda %1$d flipa náð + Myndefni: %s Aðvörun vegna efnis: %s Engin lýsing @@ -489,7 +495,9 @@ Ertu viss um að þú viljir eyða %s listanum\? Ótiltekið Tímalengd - Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. + + Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. + Fela magntölfræði notendasniða Fela magntölfræði færslna Takmarka tilkynningar á tímalínu diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c43e60f4d..3c8093557 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -216,7 +216,9 @@ %1$s, %2$s, %3$s e %4$d altri %1$s, %2$s e %3$s %1$s e %2$s - %d nuove interazioni + + %d nuove interazioni + Account bloccato A proposito Tusky %s @@ -279,7 +281,9 @@ Rimuovi un account dalla lista Pubblicando con l\'account %1$s Impostazione del sottotitolo non riuscita - Descrivi per ipovedenti\n(limite di %d caratteri) + + Descrivi per ipovedenti\n(limite di %d caratteri) + Inserisci descrizione Rimuovi Blocca account @@ -336,7 +340,9 @@ %1$s %1$s e %2$s %1$s, %2$s ed altri %3$d - limite massimo di %1$d tab raggiunto + + limite massimo di %1$d tab raggiunto + Media: %s Contenuto sensibile: %s @@ -492,5 +498,7 @@ Nuovi toots qualcuno a cui sono iscritto ha pubblicato un nuovo toot %s appena pubblicato - Non puoi caricare più di %1$d allegati multimediali. + + 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 3fe5d7f8a..9185911c3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -206,7 +206,9 @@ %1$sさん、%2$sさん、%3$sさんと他%4$d人 %1$sさん、%2$sさん、%3$sさん %1$sさん、%2$sさん - %d件の新しい通知 + + %d件の新しい通知 + 非公開アカウント このアプリについて Tusky %s @@ -258,7 +260,9 @@ リスト名の変更 %1$sで投稿 説明の設定に失敗しました - 視覚障害者のための説明 (%d文字まで) + + 視覚障害者のための説明 (%d文字まで) + 説明を設定 消去 アカウントをロック @@ -310,7 +314,9 @@ ブーストした人物 お気に入りした人物 - タブは %1$d 個が上限です + + タブは %1$d 個が上限です + 公開 未収載 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 893b4acb1..c955454a1 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -230,7 +230,9 @@ %1$s, %2$s, %3$s, 그 외 %4$d개 %1$s님, %2$s님, %3$s님 %1$s와 %2$s - %d개의 새로운 알림이 있습니다 + + %d개의 새로운 알림이 있습니다 + 계정 잠김 이 앱에 관하여 Tusky %s @@ -293,8 +295,10 @@ 리스트에서 계정 삭제 %1$s로서 포스팅 미디어에 대한 설명을 추가할 수 없습니다 - 시각 장애인을 위한 설명 -\n(%d글자 작성 가능) + + 시각 장애인을 위한 설명 +\n(%d글자 작성 가능) + 설명 추가 삭제 계정 잠금 @@ -350,7 +354,9 @@ %1$s %1$s와 %2$s %1$s, %2$s 외 %3$d명 - 최대 탭 수 %1$d에 도달했습니다 + + 최대 탭 수 %1$d에 도달했습니다 + 미디어: %s 열람주의: %s 설명 없음 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 50d94b3d7..cfa8e7f08 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -215,7 +215,9 @@ %1$s, %2$s, %3$s en %4$d anderen %1$s, %2$s en %3$s %1$s en %2$s - %d nieuwe interacties + + %d nieuwe interacties + Besloten account Over Tusky %s @@ -259,7 +261,9 @@ Tijdlijn lijst Aan het publiceren met account %1$s Toevoegen van beschrijving mislukt - Omschrijf dit voor mensen met een visuele beperking\n(tekenlimiet is %d) + + Omschrijf dit voor mensen met een visuele beperking\n(tekenlimiet is %d) + Beschrijving toevoegen Verwijderen Account besloten maken @@ -316,7 +320,9 @@ %1$s %1$s en %2$s %1$s, %2$s en %3$d meer - maximum van %1$d tabs bereikt + + maximum van %1$d tabs bereikt + Media: %s Inhoudswaarschuwing: %s diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 455f5e5d4..17fb2a1aa 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -202,7 +202,9 @@ %1$s, %2$s, %3$s og %4$d anre %1$s, %2$s, og %3$s %1$s og %2$s - %d nye interaksjoner + + %d nye interaksjoner + Låst konto Om Tusky %s @@ -259,8 +261,10 @@ om %dy Poster med konto %1$s Klarte ikke å sette bildetekst - Beskriv for de med nedsatt synsevne -\n(maks %d tegn) + + Beskriv for de med nedsatt synsevne +\n(maks %d tegn) + Sett bildetekst Slett Lås konto @@ -317,7 +321,9 @@ %1$s %1$s og %2$s %1$s, %2$s og %3$d fler - grensen på %1$d faner er nådd + + grensen på %1$d faner er nådd + Media: %s Innholdsadvarsel: %s Ingen beskrivelse @@ -483,7 +489,9 @@ Nye toots noen jeg følger publiserer en ny toot %s tootet akkurat - Du kan ikke laste opp flere enn %1$d mediavedlegg. + + Du kan ikke laste opp flere enn %1$d mediavedlegg. + Uendelig Varighet Er du sikker på at du vil slette listen %s\? diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index a404fef52..4de5640d6 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -185,7 +185,9 @@ %1$s, %2$s, %3$s e %4$d mai %1$s, %2$s e %3$s %1$s e %2$s - %d interaccions nòvas + + %d interaccions nòvas + Compte blocat A prepaus Tusky es programa gratuït, liure e de còdi dobèrt. @@ -323,8 +325,10 @@ Cercar lo monde que seguètz Ajustar un compte a la lista Suprimir aqueste compte de la lista - Descriure pels mal vesents -\n(%d caractèrs maximum) + + Descriure pels mal vesents +\n(%d caractèrs maximum) + CC-BY 4.0 CC-BY-SA 4.0 @@ -340,7 +344,9 @@ %1$s %1$s e %2$s %1$s, %2$s e %3$d mai - nombre maximum d’onglets %1$d atengut + + nombre maximum d’onglets %1$d atengut + Mèdia : %s Avertiment : %s Cap de descripcion diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 03ca93804..5ec0dbf5c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -182,7 +182,9 @@ %1$s, %2$s, %3$s i %4$d innych %1$s, %2$s, i %3$s %1$s i %2$s - %d nowych powiadomień + + %d nowych powiadomień + Konto zablokowane O programie Tusky jest wolnym i otwartoźródłowym oprogramowaniem. Jest on dostępny na licencji GNU General Public License w wersji trzeciej. Możesz przeczytać przetłumaczoną treść licencji tutaj @@ -334,8 +336,10 @@ Szukaj osób, które śledzisz Dodaj konto do listy Usuń konto z listy - Wprowadź opis dla niewidomych i niedowidzących -\n(maksymalna długość: %d) + + Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %d) + Aktualny zestaw emoji Google Bot CC-BY 4.0 @@ -360,7 +364,9 @@ %1$s %1$s i %2$s %1$s, %2$s i %3$d innych - maksymalna liczba zakładek (%1$d) osiągnięta + + maksymalna liczba zakładek (%1$d) osiągnięta + Media: %s Ostrzeżenie o zawartości: %s Brak opisu diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 939349b61..cf6cfdf2f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -202,7 +202,9 @@ %1$s, %2$s, %3$s e %4$d outros %1$s, %2$s, e %3$s %1$s e %2$s - %d novas interações + + %d novas interações + Conta trancada Sobre Tusky %s @@ -328,8 +330,10 @@ Pesquisar pessoas que você segue Adicionar conta à lista Remover conta da lista - Descrever para deficientes visuais -\n(até %d caracteres) + + Descrever para deficientes visuais +\n(até %d caracteres) + CC-BY 4.0 CC-BY-SA 4.0 As informações abaixo podem refletir incompletamente o perfil do usuário. Toque aqui para abrir o perfil completo no navegador. @@ -346,7 +350,9 @@ %1$s %1$s e %2$s %1$s, %2$s e %3$d outros - excedeu o máximo de %1$d abas + + excedeu o máximo de %1$d abas + Mídia: %s Aviso de Conteúdo: %s Sem descrição @@ -481,7 +487,9 @@ 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. + + 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 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c7f2550da..1b43d7236 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s и %4$d других %1$s, %2$s, и %3$s %1$s и %2$s - Новых событий: %d + + Новых событий: %d + Закрытый аккаунт О приложении Tusky %s @@ -305,7 +307,9 @@ Удалить аккаунт из списка Отправка от имени %1$s Не удалось добавить подпись - Описание для слабовидящих\n(не более %d символов) + + Описание для слабовидящих\n(не более %d символов) + Добавить подпись Удалить Закрыть аккаунт @@ -366,7 +370,9 @@ %1$s %1$s и %2$s %1$s, %2$s и ещё %3$d - достигнут лимит в %1$d вкладок + + достигнут лимит в %1$d вкладок + Медиафайл: %s diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index 18bb76015..168b1baf1 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -247,7 +247,9 @@ टस्की %s विज्ञप्तिः कपाटितव्यक्तिविवरणलेखः - %d नवपरस्परक्रियाः + + %d नवपरस्परक्रियाः + %1$s च %2$s च %1$s, %2$s, तथैव %3$s %1$s, %2$s, %3$s तथा च %4$d अन्येऽपि @@ -328,8 +330,10 @@ लेखा अवरुध्यताम् नश्यताम् शीर्षकवाक्यं लिख्यताम् - दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् -\n(%d परिमिता न्यूनाक्षरसङ्ख्या) + + दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् +\n(%d परिमिता न्यूनाक्षरसङ्ख्या) + शीर्षकवाक्यं लेखितुमशक्यम् %1$s लेखया प्रकटनं क्रियते सूच्याः लेखा नश्यताम् @@ -449,7 +453,9 @@ विवरणं नास्ति विषयपूर्वसतर्कता: %s सामग्र्यः %s - अधिकतमपीठिकासङ्ख्या %1$d भूता + + अधिकतमपीठिकासङ्ख्या %1$d भूता + %1$s, %2$s तथा %3$d अन्येऽपि %1$s तथैव %2$s निम्नमित्रस्य प्रीतिः diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 35b3f08ea..033c89b9b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -204,7 +204,9 @@ %1$s, %2$s, %3$s in %4$d ostali %1$s, %2$s in %3$s %1$s in %2$s - %d novih interakcij + + %d novih interakcij + Zaklenjen račun O aplikaciji Tusky %s @@ -258,8 +260,10 @@ Odstrani račun iz seznama Objavljanje z računom %1$s Opisa ni bilo mogoče nastaviti - Opišite za slabovidne -\n(omejitev znakov - %d) + + Opišite za slabovidne +\n(omejitev znakov - %d) + Nastavi opis Odstrani Zakleni račun @@ -314,7 +318,9 @@ %1$s %1$s in %2$s %1$s, %2$s in %3$d več - doseženih maksimalnih %1$d zavihkov + + doseženih maksimalnih %1$d zavihkov + Mediji: %s Opozorila o vsebini: %s Brez opisa diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 34eb3e8c1..dc62bf6d8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -217,7 +217,9 @@ %1$s, %2$s, %3$s och %4$d andra %1$s, %2$s, och %3$s %1$s och %2$s - %d nya interaktioner + + %d nya interaktioner + Låst konto Om Tusky %s @@ -280,7 +282,9 @@ Ta bort kontot från listan Inlägg med kontot %1$s Misslyckades med att ange bildtext - Beskriv för synskadade\n(%d teckengräns) + + Beskriv för synskadade\n(%d teckengräns) + Ange bildtext Ta bort Lås konto @@ -337,7 +341,9 @@ %1$s %1$s och %2$s %1$s, %2$s och %3$d mer - max antal flikar %1$d uppnådd + + max antal flikar %1$d uppnådd + Media: %s Innehållsvarning: %s diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index d726ba321..557911456 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -171,7 +171,9 @@ %1$s, %2$s, %3$s மற்றும் %4$d மற்றவர்கள் %1$s, %2$s, மற்றும் %3$s %1$s மற்றும் %2$s - %d புதிய ஊடாடுதல் + + %d புதிய ஊடாடுதல் + கணக்கு மூடப்பட்டது பற்றி Tusky(டஸ்கி) %s diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index fb5bd52b5..f0554fe76 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -71,7 +71,9 @@ ไม่มีคำอธิบาย เตือนเนื้อหา : %s สื่อ: %s - ถึงจำนวนแท็บสูงสุดคือ %1$d แล้ว + + ถึงจำนวนแท็บสูงสุดคือ %1$d แล้ว + %1$s, %2$s และอีก %3$d %1$s และ %2$s %1$s @@ -128,8 +130,10 @@ ลบ ตั้งคำอธิบายล้มเหลว ตั้งคำอธิบาย - อธิบายเพื่อผู้บกพร่องทางสายตา -\n(จำกัด %d ตัวอักขระ) + + อธิบายเพื่อผู้บกพร่องทางสายตา +\n(จำกัด %d ตัวอักขระ) + โพสต์ด้วยบัญชี %1$s ลบบัญชีออกจากรายการ เพิ่มบัญชีไปใส่รายการ @@ -183,7 +187,9 @@ ขับเคลื่อนด้วย Tusky Tusky %s บัญชีไม่สาธารณะ - การโต้ตอบใหม่จำนวน %d + + การโต้ตอบใหม่จำนวน %d + %1$s และ %2$s %1$s, %2$s, และ %3$s %1$s, %2$s, %3$s และอีก %4$d คน @@ -478,7 +484,9 @@ ล้มเหลวในการโหลดข้อมูลตอบกลับ ฉบับร่างเก่า คุณต้องการลบลิสต์ %s ใช่ไหม\? - คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + + คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + บันทึกแล้ว! ไม่มีประกาศ ไม่มีกำหนด diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 684ecc21e..87638c87f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -201,7 +201,9 @@ %1$s, %2$s, %3$s ve %4$d diğer %1$s, %2$s ve %3$s %1$s ve %2$s - %d yeni etkileşim + + %d yeni etkileşim + Kitli Hesap Hakkında Tusky %s @@ -243,8 +245,10 @@ Listeler Zaman çizelgesini listele %1$s hesabıyla gönderiliyor - Görsel engelli için tanımla -\n(%d karakter limiti) + + Görsel engelli için tanımla +\n(%d karakter limiti) + Başlık belirle Kaldır Hesabı Kilitle @@ -299,7 +303,9 @@ %1$s %1$s ve %2$s %1$s, %2$s ve %3$d daha fazlası - %1$d maksimum sekme sayısına ulaşıldı + + %1$d maksimum sekme sayısına ulaşıldı + Gizli alanadları Boostu kaldır Favoriyi kaldır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5b0c14c8c..e498fa6de 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -186,8 +186,10 @@ Зберегти чернетку\? Вимагає затвердження підписників власноруч Додати підпис - Опис для людей з порушеннями зору -\n(до %d символів) + + Опис для людей з порушеннями зору + \n(до %d символів) + Не вдалося додати підпис Відписатися Підписатися @@ -329,7 +331,9 @@ Створено Tusky Tusky %s Заблокований обліковий запис - %d нових взаємодій + + %d нових взаємодій + %1$s, %2$s та ще %3$d %1$s та %2$s %1$s @@ -456,7 +460,9 @@ \n Ви все ще можете отримати доступ до своїх старих чернеток за допомогою кнопки на екрані нових чернеток, але вони будуть вилучені в майбутньому оновленні! Не вдалося надіслати цей дмух! Ви дійсно хочете видалити список %s\? - Ви не можете завантажити більше %1$d медіавкладень. + + Ви не можете завантажити більше %1$d медіавкладень. + Приховати кількісну статистику профілів Приховати кількісну статистику дописів Обмеження сповіщень стрічки @@ -493,7 +499,9 @@ Безпосередньо Додано до закладок Просунуто - досягнено обмеження %1$d вкладок + + досягнено обмеження %1$d вкладок + %s просування %s просування diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f3aa8bfa9..7f41d963e 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -5,7 +5,9 @@ Hủy đăng Đăng Tút Đang đăng… - %d tương tác mới + + %d tương tác mới + %1$s và %2$s %1$s, %2$s, và %3$s %1$s, %2$s, %3$s và %4$d người khác @@ -383,7 +385,9 @@ Đã chia sẻ Không có mô tả Nội dung nhạy cảm: %s - tối đa %1$d tab + + tối đa %1$d tab + %1$s, %2$s và %3$d người nữa %1$s và %2$s %1$s @@ -433,8 +437,10 @@ Tài khoản riêng tư Hủy bỏ Mô tả - Mô tả dành cho người khiếm thị -\n(giới hạn %d chữ) + + Mô tả dành cho người khiếm thị +\n(giới hạn %d chữ) + Đăng bằng tài khoản %1$s Thêm tài khoản vào danh sách Xóa tài khoản khỏi danh sách @@ -475,7 +481,9 @@ 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. + + 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\? diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2f2a8c09d..7884d24d5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -223,7 +223,9 @@ %1$s,%2$s,%3$s 和 %4$d 等人 %1$s,%2$s 和 %3$s %1$s 和 %2$s - %d 个新互动 + + %d 个新互动 + 锁嘟用户 关于 Tusky Tusky %s @@ -288,8 +290,10 @@ 从列表中移除用户 以 %1$s 发布嘟文 设置图片标题失败 - 为视觉障碍用户提供的描述 -\n(限制 %d 字) + + 为视觉障碍用户提供的描述 +\n(限制 %d 字) + 设置图片标题 移除 保护你的帐户(锁嘟) @@ -345,7 +349,9 @@ %1$s %1$s 和 %2$s %1$s,%2$s 和 %3$d 等人 - 标签页不能超过 %1$d 个 + + 标签页不能超过 %1$d 个 + 媒体:%s 内容提醒:%s @@ -491,7 +497,9 @@ \n 旧草稿依然可以通过新草稿页面的按钮查看,但他们将在未来版本中移除! 嘟文发送失败! 确认删除列表 %s? - 最多只可上传 %1$d 个媒体附件 + + 最多只可上传 %1$d 个媒体附件 + 隐藏账号的统计信息 反馈通知 隐藏嘟文的统计信息 @@ -507,8 +515,7 @@ 永久 持续时间 - %s 人 - + %s 人 附件 音频 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 58c638919..94bb613b8 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -223,7 +223,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -287,7 +289,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -343,7 +347,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s @@ -448,7 +454,9 @@ 舊的草稿 這條嘟文發送失敗! 你確定要刪除列表 %s? - 你無法上傳超過 %1$d 媒體附件。 + + 你無法上傳超過 %1$d 媒體附件。 + 已儲存! 你對此帳號的個人註記 隱藏頂端工具列的標題 @@ -485,8 +493,7 @@ 返回 繼續 - %s 人 - + %s 人 列表 選擇列表 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 538284c60..7fe854602 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -217,7 +217,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -281,7 +283,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -337,7 +341,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 2f45c629d..23c149097 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 个新互动 + + %d 个新互动 + 锁嘟用户 关于 Tusky Tusky %s @@ -285,7 +287,9 @@ 从列表中移除用户 以 %1$s 发布嘟文 设置图片标题失败 - 为视觉障碍用户提供的描述\n(限制 %d 字) + + 为视觉障碍用户提供的描述\n(限制 %d 字) + 设置图片标题 移除 保护你的帐户(锁嘟) @@ -341,7 +345,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 标签页不能超过 %1$d 个 + + 标签页不能超过 %1$d 个 + 媒体: %s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 46411116b..ee1754389 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -223,7 +223,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -287,7 +289,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -343,7 +347,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s @@ -449,8 +455,7 @@ \n推播通知不會受到影響,但你可以手動檢查你的通知設定。 數位健康 - %s 人 - + %s 人 %s 剛剛發了新嘟文 %s 請求關注你 @@ -472,7 +477,9 @@ 5 分鐘 無限期 期間 - 你無法上傳超過 %1$d 媒體附件。 + + 你無法上傳超過 %1$d 媒體附件。 + 當你關注的人發布新嘟文時通知 新嘟文 我關注的人有新嘟文 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6f05b4f2..ce2cb0dda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -297,7 +297,10 @@ %1$s, %2$s, %3$s and %4$d others %1$s, %2$s, and %3$s %1$s and %2$s - %d new interactions + + %d new interaction + %d new interactions + Locked Account @@ -381,7 +384,9 @@ Posting with account %1$s Failed to set caption - Describe for visually impaired\n(%d character limit) + + Describe for visually impaired\n(%d character limit) + Set caption Remove Lock account @@ -452,8 +457,10 @@ %1$s %1$s and %2$s %1$s, %2$s and %3$d more - maximum of %1$d tabs reached - + + maximum of %1$d tab reached + maximum of %1$d tabs reached + Media: %s @@ -597,7 +604,10 @@ Limit timeline notifications Hide quantitative stats on posts Hide quantitative stats on profiles - You cannot upload more than %1$d media attachments. + + You cannot upload more than %1$d media attachment. + You cannot upload more than %1$d media attachments. + Do you really want to delete the list %s? This toot failed to send! From a6fd787a173ba0adda1aa03b239c859089004168 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Wed, 10 Mar 2021 13:01:04 +0000 Subject: [PATCH 15/41] Translated using Weblate (Persian) Currently translated at 98.4% (452 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ --- app/src/main/res/values-fa/strings.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 82e77ff96..fc268acf7 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -193,6 +193,7 @@ %1$s، %2$s و %3$s %1$s و %2$s + %d برهم‌کنش جدید %d برهم‌کنش جدید حساب قفل‌شده @@ -357,6 +358,7 @@ %1$s و %2$s %1$s، %2$s و %3$d بیش‌تر + رسیده به بیشینهٔ %1$d زبانه رسیده به بیشینهٔ %1$d زبانه رسانه: %s @@ -480,4 +482,22 @@ یادداشت خصوصیتان دربارهٔ این حساب هیچ اعلامیه‌ای وجود ندارد. اعلامیه‌ها + عدم اشتراک + اشتراک + پیش‌نویس حذف شد + پیش‌نویس‌های قدیمی + فرستادن این بوق شکست خورد! + نهفتن آمار کمی روی نمایه‌ها + نهفتن آمار کمی روی فرسته‌ها + محدود کردن آگاهی‌های خط‌زمانی + بازبینی آگاهی‌ها + سلامتی + طول + پیوست‌ها + صدا + آگاهی‌ها هنگام انتشار بوقی جدید از کسی که مشترکش هستید + بوق‌های جدید + اموجی‌های شخصی متحرّک + کسی که مشترکش شده‌ام، بوقی جدید منتشر کرد + %s چیزی فرستاد \ No newline at end of file From e5808773b1ddb547a09e2057c966daaa20b9f2ca Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Wed, 10 Mar 2021 13:01:04 +0000 Subject: [PATCH 16/41] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ --- app/src/main/res/values-no-rNB/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 17fb2a1aa..ca5a20f4e 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -203,6 +203,7 @@ %1$s, %2$s, og %3$s %1$s og %2$s + %d ny interaksjon %d nye interaksjoner Låst konto @@ -322,6 +323,7 @@ %1$s og %2$s %1$s, %2$s og %3$d fler + grensen på %1$d fane er nådd grensen på %1$d faner er nådd Media: %s @@ -490,6 +492,7 @@ noen jeg følger publiserer en ny toot %s tootet akkurat + Du kan ikke laste opp flere enn %1$d mediavedlegg. Du kan ikke laste opp flere enn %1$d mediavedlegg. Uendelig From 83e8bc70356f24b77252af73f4cffd5263d37199 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Wed, 10 Mar 2021 13:01:04 +0000 Subject: [PATCH 17/41] Translated using Weblate (Gaelic) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ --- app/src/main/res/values-gd/strings.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 8e49477a0..c60d0be69 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -75,7 +75,10 @@ Gun chrìoch Faide - Chan urrainn dhut barrachd air %1$d ceanglacha(i)n meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d ceanglachain meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d ceanglachan meadhain a luchdadh suas. Falaich an stadastaireachd àireamhail air pròifilean Falaich an stadastaireachd àireamhail air postaichean @@ -187,7 +190,10 @@ Rabhadh susbainte: %s Meadhan: %s - ràinig thu na tha ceadaichte dhe %1$d taba(ichean) + ràinig thu na tha ceadaichte dhe %1$d taba + ràinig thu na tha ceadaichte dhe %1$d thaba + ràinig thu na tha ceadaichte dhe %1$d tabaichean + ràinig thu na tha ceadaichte dhe %1$d taba %1$s, %2$s ’s %3$d eile %1$s @@ -332,7 +338,10 @@ Tusky %s Cunntas glaiste - %d eadar-ghabhail(ean) ùr(a) + %d eadar-ghabhail ùr + %d eadar-ghabhail ùr + %d eadar-ghabhailean ùra + %d eadar-ghabhail ùr %1$s ’s %2$s %1$s, %2$s ’s %3$s From 9efd3ca01731d1e34eeb211ea2d89c83ec1c5b3c Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 10 Mar 2021 13:01:05 +0000 Subject: [PATCH 18/41] Translated using Weblate (Galician) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ --- app/src/main/res/values-gl/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 504e1010f..da0d2bc1d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -201,6 +201,7 @@ Aviso sobre o contido: %s Multimedia: %s + acadouse o máximo de %1$d lapela acadouse o máximo de %1$d lapelas %1$s, %2$s e %3$d máis @@ -323,6 +324,7 @@ Acerca de Conta bloqueada + %d nova interacción %d novas interaccións %1$s e %2$s @@ -442,6 +444,7 @@ Fallou o envío do toot! Tes a certeza de querer eliminar a listaxe %s\? + Non podes subir máis de %1$d anexo multimedia. Non podes subir máis de %1$d anexos multimedia. Agochar estatísticas cuantitativas nos perfís From a30ee83bed644b8da3763a9b36b65eac40d6e52b Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 10 Mar 2021 13:01:05 +0000 Subject: [PATCH 19/41] Translated using Weblate (Ukrainian) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e498fa6de..73882f1ea 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -332,6 +332,9 @@ Tusky %s Заблокований обліковий запис + %d нова взаємодія + %d нові взаємодії + %d нових взаємодій %d нових взаємодій %1$s, %2$s та ще %3$d @@ -430,7 +433,7 @@ \n \nДокладніше на joinmastodon.org. Що таке сервер\? - Що таке сервер\? + Котрий сервер\? Заголовок Аватар Відповісти… @@ -461,7 +464,10 @@ Не вдалося надіслати цей дмух! Ви дійсно хочете видалити список %s\? - Ви не можете завантажити більше %1$d медіавкладень. + Ви не можете завантажити більше ніж %1$d медіавкладення. + Ви не можете завантажити більше ніж %1$d медіавкладення. + Ви не можете завантажити більше ніж %1$d медіавкладень. + Ви не можете завантажити більше ніж %1$d медіавкладень. Приховати кількісну статистику профілів Приховати кількісну статистику дописів @@ -500,6 +506,9 @@ Додано до закладок Просунуто + досягнено обмеження %1$d вкладка + досягнено обмеження %1$d вкладки + досягнено обмеження %1$d вкладок досягнено обмеження %1$d вкладок From 89d3cf9d351055b45c64ea44c182ed5d403477e0 Mon Sep 17 00:00:00 2001 From: Ru Mac Date: Wed, 10 Mar 2021 13:01:05 +0000 Subject: [PATCH 20/41] Translated using Weblate (Gaelic) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ --- app/src/main/res/values-gd/strings.xml | 76 +++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index c60d0be69..6d35b7af2 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -26,14 +26,14 @@ Freagair… Lorg… Clàraich a-steach le Mastodon - DÙD! + TÙT! Meur-chlàr Emoji Na lean tuilleadh Lean Barrachd Feuch ris a-rithist Dùin - DÙD + TÙT Sguab às Sguab às is dèan dreachd ùr air Dèan gearan @@ -55,13 +55,13 @@ Cuir crìoch air an fho-sgrìobhadh Fo-sgrìobh Beòthaich na h-Emojis gnàthaichte - Bha againn ris an dùd a bha thu airson freagairt dha a thoirt air falbh + Bha againn ris an tùt a bha thu airson freagairt dha a thoirt air falbh Chaidh an dreach a sguabadh às Cha deach leinn fiosrachadh na freagairte a luchdadh Seann-dreachdan Chaidh dealbhadh gu tur ùr a chur air gleus nan dreachdan aig Tusky ach am biodh e nas luaithe, nas fhasa cleachdadh is nas lugha de bhugaichean ann. \n Gheibh thu grèim air na seann-dreachdan agad fhathast le putan air sgrìn ùr nan dreachdan ach thèid an toirt air falbh le ùrachadh ri teachd! - Cha b’ urrainn dhuinn an dùd a chur! + Cha b’ urrainn dhuinn an tùt a chur! Ceanglachain Fuaim A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? @@ -84,17 +84,17 @@ Falaich an stadastaireachd àireamhail air postaichean Cuingich na brathan mun loidhne-ama Thoir sùil air na brathan - Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: -\n -\n - Brathan air annsachdan/brosnachaidhean/leanntainn -\n - Cunntas nan annsachdan/brosnachaidhean air dùdaidhean -\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean -\n + Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: +\n +\n - Brathan air annsachdan/brosnachaidhean/leanntainn +\n - Cunntas nan annsachdan/brosnachaidhean air tùtaidhean +\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean +\n \n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh. Slàinte-inntinn - Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr - Dùdaidhean ùra - dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr + Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh tùt ùr + Tùtaidhean ùra + dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh tùt ùr Tha %s air rud a phostadh Chan eil brath-fios ann. Brathan-fios @@ -172,7 +172,7 @@ %1$s • %2$s Gnìomhan dhan dealbh %s A bheil thu cinnteach gu bheil thu airson na brathan uile agad fhalamhachadh gu buan\? - Sgrìobh dùd + Sgrìobh tùt Cuir an sàs Criathraich Falamhaich @@ -226,7 +226,7 @@ Na brosnaich tuilleadh Brosnaich dhan èisteachd tùsail Chaidh %1$s a ghluasad gu: - Bot + Robotair Dh’fhàillig an luchdadh a-nuas Seata làithreach nan Emoji aig Google Seata stannardach nan Emoji aig Mastodon @@ -236,7 +236,7 @@ Uaireigin eile Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs Feumaidh tu an aplacaid ath-thòiseachadh - Fosgail an dùd + Fosgail an tùt Leudaich/Co-theannaich gach staid ’Ga lorg… Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach @@ -244,11 +244,11 @@ Stoidhle nan Emojis Chaidh lethbhreac dheth a chur air an stòr-bhòrd Chan eil Emojis gnàthaichte aig an ionstans %s agad - Chaidh lethbhreac dhen dùd agad a shàbhaladh ’na dhreachd + Chaidh lethbhreac dhen tùt agad a shàbhaladh ’na dhreachd Chaidh sgur dhen chur - A’ cur nan dùd - Mearachd a’ cur an dùd - A’ cur an dùd… + A’ cur nan tùt + Mearachd a’ cur an tùt + A’ cur an tùt… A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas @@ -273,14 +273,14 @@ Cuir cunntas ris An abairt ri chriathradh Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin - Leudaich dùdaidhean le rabhaidhean susbainte an-còmhnaidh - Co-roinn ceangal dhan dùd - Co-roinn susbaint an dùd + Leudaich tùtaidhean le rabhaidhean susbainte an-còmhnaidh + Co-roinn ceangal dhan tùt + Co-roinn susbaint an tùt ’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html - Brathan nuair a thèid dùd agad a chomharrachadh ’na annsachd - Brathan nuair a thèid dùd agad brosnachadh - A bheil thu airson an dùd seo a sguabadh às is dreachd ùr a dhèanamh air\? - A bheil thu airson an dùd seo a sguabadh às\? + Brathan nuair a thèid tùt agad a chomharrachadh ’na annsachd + Brathan nuair a thèid tùt agad brosnachadh + A bheil thu airson an tùt seo a sguabadh às is dreachd ùr a dhèanamh air\? + A bheil thu airson an tùt seo a sguabadh às\? ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! \n \nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. @@ -288,16 +288,16 @@ \n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. \n \nGheibh thu barrachd fiosrachaidh air joinmastodon.org. - Co-roinn an dùd le… - Co-roinn URL an dùd le… - Cuir dùd air an sgeideal - Faicsinneachd an dùd - Dùdaidhean air an sgeideal - Chuir %s an dùd agad ris na h-annsachdan - Bhrosnaich %s an dùd agad - Dùdaidhean air an sgeideal - Dùd - Mearachd a’ cur an dùd. + Co-roinn an tùt le… + Co-roinn URL an tùt le… + Cuir tùt air an sgeideal + Faicsinneachd an tùt + Tùtaidhean air an sgeideal + Chuir %s an tùt agad ris na h-annsachdan + Bhrosnaich %s an tùt agad + Tùtaidhean air an sgeideal + Tùt + Mearachd a’ cur an tùt. Dì-mhùch %s Tagaichean hais Luchd-leantainn @@ -389,7 +389,7 @@ Cleachd tabaichean Chrome gnàthaichte Brabhsair Cleachd co-dhealbhachd an t-siostaim - Gu fèin-obrachail aig laighe na grèine + Gu fèin-obrachail aig beul na h-oidhche Dubh Soilleir Dorcha From bea5098cc1fb794abc19592d1d46a80630f236c0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 13 Mar 2021 21:27:20 +0100 Subject: [PATCH 21/41] migrating to ViewBinding part 4: Fragments (#2108) * migrating to ViewBinding part 4: Fragment * fix imports * don't use viewBinding extension in ViewImage and ViewVideoFragment * don't use viewBinding extension in ViewImage and ViewVideoFragment --- app/build.gradle | 1 + .../tusky/AccountsInListFragment.kt | 189 ++++++++---------- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 + .../conversation/ConversationsFragment.kt | 27 +-- .../fragment/InstanceListFragment.kt | 46 +++-- .../report/fragments/ReportDoneFragment.kt | 36 ++-- .../report/fragments/ReportNoteFragment.kt | 49 ++--- .../fragments/ReportStatusesFragment.kt | 39 ++-- .../fragments/SearchAccountsFragment.kt | 4 +- .../search/fragments/SearchFragment.kt | 37 ++-- .../fragments/SearchStatusesFragment.kt | 9 +- .../tusky/fragment/AccountListFragment.kt | 40 ++-- .../tusky/fragment/AccountMediaFragment.kt | 85 ++++---- .../tusky/fragment/ViewImageFragment.kt | 60 +++--- .../tusky/fragment/ViewVideoFragment.kt | 74 ++++--- .../tusky/util/ViewBindingExtensions.kt | 56 +++++- app/src/main/res/layout/fragment_search.xml | 1 - app/src/main/res/layout/fragment_timeline.xml | 5 +- 18 files changed, 412 insertions(+), 349 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9ab2647c3..583abb11d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,6 +125,7 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index f1c3d54dd..0151e070c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -23,11 +23,13 @@ import android.view.ViewGroup import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account @@ -38,9 +40,6 @@ import com.keylesspalace.tusky.viewmodel.State import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.fragment_accounts_in_list.* -import kotlinx.android.synthetic.main.item_follow_request.* import java.io.IOException import javax.inject.Inject @@ -48,23 +47,11 @@ private typealias AccountInfo = Pair class AccountsInListFragment : DialogFragment(), Injectable { - companion object { - private const val LIST_ID_ARG = "listId" - private const val LIST_NAME_ARG = "listName" - - @JvmStatic - fun newInstance(listId: String, listName: String): AccountsInListFragment { - val args = Bundle().apply { - putString(LIST_ID_ARG, listId) - putString(LIST_NAME_ARG, listName) - } - return AccountsInListFragment().apply { arguments = args } - } - } - @Inject lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: AccountsInListViewModel + + private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(FragmentAccountsInListBinding::bind) private lateinit var listId: String private lateinit var listName: String @@ -79,7 +66,6 @@ class AccountsInListFragment : DialogFragment(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) - viewModel = viewModelFactory.create(AccountsInListViewModel::class.java) val args = requireArguments() listId = args.getString(LIST_ID_ARG)!! listName = args.getString(LIST_NAME_ARG)!! @@ -100,12 +86,11 @@ class AccountsInListFragment : DialogFragment(), Injectable { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - accountsRecycler.layoutManager = LinearLayoutManager(view.context) - accountsRecycler.adapter = adapter + binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsRecycler.adapter = adapter - accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) - accountsSearchRecycler.adapter = searchAdapter + binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsSearchRecycler.adapter = searchAdapter viewModel.state .observeOn(AndroidSchedulers.mainThread()) @@ -114,15 +99,15 @@ class AccountsInListFragment : DialogFragment(), Injectable { adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) when (state.accounts) { - is Either.Right -> messageView.hide() + is Either.Right -> binding.messageView.hide() is Either.Left -> handleError(state.accounts.value) } setupSearchView(state) } - searchView.isSubmitButtonEnabled = true - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding.searchView.isSubmitButtonEnabled = true + binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { viewModel.search(query ?: "") return true @@ -141,30 +126,30 @@ class AccountsInListFragment : DialogFragment(), Injectable { private fun setupSearchView(state: State) { if (state.searchResult == null) { searchAdapter.submitList(listOf()) - accountsSearchRecycler.hide() - accountsRecycler.show() + binding.accountsSearchRecycler.hide() + binding.accountsRecycler.show() } else { val listAccounts = state.accounts.asRightOrNull() ?: listOf() val newList = state.searchResult.map { acc -> acc to listAccounts.contains(acc) } searchAdapter.submitList(newList) - accountsSearchRecycler.show() - accountsRecycler.hide() + binding.accountsSearchRecycler.show() + binding.accountsRecycler.hide() } } private fun handleError(error: Throwable) { - messageView.show() + binding.messageView.show() val retryAction = { _: View -> - messageView.hide() + binding.messageView.hide() viewModel.load(listId) } if (error is IOException) { - messageView.setup(R.drawable.elephant_offline, + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network, retryAction) } else { - messageView.setup(R.drawable.elephant_error, + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic, retryAction) } } @@ -187,39 +172,28 @@ class AccountsInListFragment : DialogFragment(), Injectable { } } - inner class Adapter : ListAdapter(AccountDiffer) { + inner class Adapter : ListAdapter>(AccountDiffer) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_follow_request, parent, false) - return ViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) + + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + onRemoveFromList(getItem(holder.adapterPosition).id) + } + binding.rejectButton.contentDescription = + binding.root.context.getString(R.string.action_remove_from_list) + + return holder } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), - View.OnClickListener, LayoutContainer { - - override val containerView = itemView - - init { - acceptButton.hide() - rejectButton.setOnClickListener(this) - rejectButton.contentDescription = - itemView.context.getString(R.string.action_remove_from_list) - } - - fun bind(account: Account) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) - usernameTextView.text = account.username - loadAvatar(account.avatar, avatar, radius, animateAvatar) - } - - override fun onClick(v: View?) { - onRemoveFromList(getItem(adapterPosition).id) - } + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val account = getItem(position) + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) } } @@ -232,57 +206,58 @@ class AccountsInListFragment : DialogFragment(), Injectable { return oldItem.second == newItem.second && oldItem.first.deepEquals(newItem.first) } - } - inner class SearchAdapter : ListAdapter(SearchDiffer) { + inner class SearchAdapter : ListAdapter>(SearchDiffer) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_follow_request, parent, false) - return ViewHolder(view) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val (account, inAList) = getItem(position) - holder.bind(account, inAList) - - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), - View.OnClickListener, LayoutContainer { - - override val containerView = itemView - - fun bind(account: Account, inAList: Boolean) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) - usernameTextView.text = account.username - loadAvatar(account.avatar, avatar, radius, animateAvatar) - - rejectButton.apply { - contentDescription = if (inAList) { - setImageResource(R.drawable.ic_reject_24dp) - getString(R.string.action_remove_from_list) - } else { - setImageResource(R.drawable.ic_plus_24dp) - getString(R.string.action_add_to_list) - } - } - } - - init { - acceptButton.hide() - rejectButton.setOnClickListener(this) - } - - override fun onClick(v: View?) { - val (account, inAList) = getItem(adapterPosition) + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + val (account, inAList) = getItem(holder.adapterPosition) if (inAList) { onRemoveFromList(account.id) } else { onAddToList(account) } } + + return holder + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val (account, inAList) = getItem(position) + + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) + + holder.binding.rejectButton.apply { + contentDescription = if (inAList) { + setImageResource(R.drawable.ic_reject_24dp) + getString(R.string.action_remove_from_list) + } else { + setImageResource(R.drawable.ic_plus_24dp) + getString(R.string.action_add_to_list) + } + } + } + } + + companion object { + private const val LIST_ID_ARG = "listId" + private const val LIST_NAME_ARG = "listName" + + @JvmStatic + fun newInstance(listId: String, listName: String): AccountsInListFragment { + val args = Bundle().apply { + putString(LIST_ID_ARG, listId) + putString(LIST_NAME_ARG, listName) + } + return AccountsInListFragment().apply { arguments = args } } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 4d5a124cf..86205b29f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -69,6 +69,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener private val binding by viewBinding(ActivityViewMediaBinding::inflate) + val toolbar: View + get() = binding.toolbar + var isToolbarVisible = true private set 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 abae8702b..009c62f61 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 @@ -28,6 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment @@ -38,7 +39,7 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.fragment_timeline.* +import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @@ -48,6 +49,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(FragmentTimelineBinding::bind) + private lateinit var adapter: ConversationAdapter private var layoutManager: LinearLayoutManager? = null @@ -73,14 +76,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) - recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) layoutManager = LinearLayoutManager(view.context) - recyclerView.layoutManager = layoutManager - recyclerView.adapter = adapter - (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.adapter = adapter + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - progressBar.hide() - statusView.hide() + binding.progressBar.hide() + binding.statusView.hide() initSwipeToRefresh() @@ -97,16 +100,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private fun initSwipeToRefresh() { viewModel.refreshState.observe(viewLifecycleOwner) { - swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING + binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING } - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() } - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } private fun onTopLoaded() { - recyclerView.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) } override fun onReblog(reblog: Boolean, position: Int) { @@ -183,7 +186,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private fun jumpToTop() { if (isAdded) { layoutManager?.scrollToPosition(0) - recyclerView.stopScroll() + binding.recyclerView.stopScroll() } } 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 093fc42d3..005432d88 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 @@ -2,9 +2,7 @@ package com.keylesspalace.tusky.components.instancemute.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.recyclerview.widget.DividerItemDecoration @@ -14,16 +12,17 @@ import com.google.android.material.snackbar.Snackbar 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.databinding.FragmentInstanceListBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.fragment_instance_list.* import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -31,9 +30,12 @@ import java.io.IOException import javax.inject.Inject class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { + @Inject lateinit var api: MastodonApi + private val binding by viewBinding(FragmentInstanceListBinding::bind) + private var fetching = false private var bottomId: String? = null private var adapter = DomainMutesAdapter(this) @@ -42,12 +44,12 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.setHasFixedSize(true) - recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recyclerView.adapter = adapter val layoutManager = LinearLayoutManager(view.context) - recyclerView.layoutManager = layoutManager + binding.recyclerView.layoutManager = layoutManager scrollListener = object : EndlessOnScrollListener(layoutManager) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { @@ -57,7 +59,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl } } - recyclerView.addOnScrollListener(scrollListener) + binding.recyclerView.addOnScrollListener(scrollListener) fetchInstances() } @@ -85,7 +87,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { adapter.removeItem(position) - Snackbar.make(recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { mute(true, instance, position) } @@ -103,10 +105,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl return } fetching = true - instanceProgressBar.show() + binding.instanceProgressBar.show() if (id != null) { - recyclerView.post { adapter.bottomLoading = true } + binding.recyclerView.post { adapter.bottomLoading = true } } api.domainBlocks(id, bottomId) @@ -116,7 +118,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl val instances = response.body() if (response.isSuccessful && instances != null) { - onFetchInstancesSuccess(instances, response.headers().get("Link")) + onFetchInstancesSuccess(instances, response.headers()["Link"]) } else { onFetchInstancesFailure(Exception(response.message())) } @@ -127,7 +129,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl private fun onFetchInstancesSuccess(instances: List, linkHeader: String?) { adapter.bottomLoading = false - instanceProgressBar.hide() + binding.instanceProgressBar.hide() val links = HttpHeaderLink.parse(linkHeader) val next = HttpHeaderLink.findByRelationType(links, "next") @@ -137,32 +139,32 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl fetching = false if (adapter.itemCount == 0) { - messageView.show() - messageView.setup( + binding.messageView.show() + binding.messageView.setup( R.drawable.elephant_friend_empty, R.string.message_empty, null ) } else { - messageView.hide() + binding.messageView.hide() } } private fun onFetchInstancesFailure(throwable: Throwable) { fetching = false - instanceProgressBar.hide() + binding.instanceProgressBar.hide() Log.e(TAG, "Fetch failure", throwable) if (adapter.itemCount == 0) { - messageView.show() + binding.messageView.show() if (throwable is IOException) { - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.hide() this.fetchInstances(null) } } else { - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.hide() this.fetchInstances(null) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 03bd8ef91..794cb287b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -22,12 +22,13 @@ import androidx.fragment.app.activityViewModels import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.fragment_report_done.* +import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { @@ -37,8 +38,10 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportDoneBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) + binding.textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) handleClicks() subscribeObservables() } @@ -46,14 +49,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { private fun subscribeObservables() { viewModel.muteState.observe(viewLifecycleOwner) { if (it !is Loading) { - buttonMute.show() - progressMute.show() + binding.buttonMute.show() + binding.progressMute.show() } else { - buttonMute.hide() - progressMute.hide() + binding.buttonMute.hide() + binding.progressMute.hide() } - buttonMute.setText(when (it.data) { + binding.buttonMute.setText(when (it.data) { true -> R.string.action_unmute else -> R.string.action_mute }) @@ -61,14 +64,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { viewModel.blockState.observe(viewLifecycleOwner) { if (it !is Loading) { - buttonBlock.show() - progressBlock.show() + binding.buttonBlock.show() + binding.progressBlock.show() } - else{ - buttonBlock.hide() - progressBlock.hide() + else { + binding.buttonBlock.hide() + binding.progressBlock.hide() } - buttonBlock.setText(when (it.data) { + binding.buttonBlock.setText(when (it.data) { true -> R.string.action_unblock else -> R.string.action_block }) @@ -77,13 +80,13 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { } private fun handleClicks() { - buttonDone.setOnClickListener { + binding.buttonDone.setOnClickListener { viewModel.navigateTo(Screen.Finish) } - buttonBlock.setOnClickListener { + binding.buttonBlock.setOnClickListener { viewModel.toggleBlock() } - buttonMute.setOnClickListener { + binding.buttonMute.setOnClickListener { viewModel.toggleMute() } } @@ -91,5 +94,4 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { companion object { fun newInstance() = ReportDoneFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index b933b2fa7..b47b586a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -24,10 +24,10 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.fragment_report_note.* import java.io.IOException import javax.inject.Inject @@ -38,6 +38,8 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportNoteBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { fillViews() handleChanges() @@ -46,29 +48,29 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun handleChanges() { - editNote.doAfterTextChanged { + binding.editNote.doAfterTextChanged { viewModel.reportNote = it?.toString() ?: "" } - checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> + binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> viewModel.isRemoteNotify = isChecked } } private fun fillViews() { - editNote.setText(viewModel.reportNote) + binding.editNote.setText(viewModel.reportNote) if (viewModel.isRemoteAccount){ - checkIsNotifyRemote.show() - reportDescriptionRemoteInstance.show() + binding.checkIsNotifyRemote.show() + binding.reportDescriptionRemoteInstance.show() } else{ - checkIsNotifyRemote.hide() - reportDescriptionRemoteInstance.hide() + binding.checkIsNotifyRemote.hide() + binding.reportDescriptionRemoteInstance.hide() } if (viewModel.isRemoteAccount) - checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) - checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify + binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) + binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify } private fun subscribeObservables() { @@ -83,13 +85,13 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun showError(error: Throwable?) { - editNote.isEnabled = true - checkIsNotifyRemote.isEnabled = true - buttonReport.isEnabled = true - buttonBack.isEnabled = true - progressBar.hide() + binding.editNote.isEnabled = true + binding.checkIsNotifyRemote.isEnabled = true + binding.buttonReport.isEnabled = true + binding.buttonBack.isEnabled = true + binding.progressBar.hide() - Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) .apply { setAction(R.string.action_retry) { sendReport() @@ -103,19 +105,19 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun showLoading() { - buttonReport.isEnabled = false - buttonBack.isEnabled = false - editNote.isEnabled = false - checkIsNotifyRemote.isEnabled = false - progressBar.show() + binding.buttonReport.isEnabled = false + binding.buttonBack.isEnabled = false + binding.editNote.isEnabled = false + binding.checkIsNotifyRemote.isEnabled = false + binding.progressBar.show() } private fun handleClicks() { - buttonBack.setOnClickListener { + binding.buttonBack.setOnClickListener { viewModel.navigateTo(Screen.Back) } - buttonReport.setOnClickListener { + binding.buttonReport.setOnClickListener { sendReport() } } @@ -123,5 +125,4 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { companion object { fun newInstance() = ReportNoteFragment() } - } 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 8ffe243e5..01a12c23c 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 @@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.adapter.AdapterHandler import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter +import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -44,8 +45,8 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData -import kotlinx.android.synthetic.main.fragment_report_statuses.* import javax.inject.Inject class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { @@ -58,6 +59,8 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportStatusesBinding::bind) + private lateinit var adapter: StatusesAdapter private var snackbarErrorRetry: Snackbar? = null @@ -93,9 +96,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje } private fun setupSwipeRefreshLayout() { - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { snackbarErrorRetry?.dismiss() viewModel.refreshStatuses() } @@ -118,10 +121,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) - recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - recyclerView.layoutManager = LinearLayoutManager(requireContext()) - recyclerView.adapter = adapter - (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false viewModel.statuses.observe(viewLifecycleOwner) { adapter.submitList(it) @@ -129,9 +132,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje viewModel.networkStateAfter.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - progressBarBottom.show() + binding.progressBarBottom.show() else - progressBarBottom.hide() + binding.progressBarBottom.hide() if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) @@ -139,22 +142,22 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje viewModel.networkStateBefore.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - progressBarTop.show() + binding.progressBarTop.show() else - progressBarTop.hide() + binding.progressBarTop.hide() if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) } viewModel.networkStateRefresh.observe(viewLifecycleOwner) { - if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) - progressBarLoading.show() + if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing) + binding.progressBarLoading.show() else - progressBarLoading.hide() + binding.progressBarLoading.hide() if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) - swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.isRefreshing = false if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) } @@ -162,7 +165,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { if (snackbarErrorRetry?.isShown != true) { - snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { viewModel.retryStatusLoad() } @@ -172,11 +175,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun handleClicks() { - buttonCancel.setOnClickListener { + binding.buttonCancel.setOnClickListener { viewModel.navigateTo(Screen.Back) } - buttonContinue.setOnClickListener { + binding.buttonContinue.setOnClickListener { viewModel.navigateTo(Screen.Note) } } 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 c453f97c5..8715e1ab2 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 @@ -23,11 +23,10 @@ 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 { - val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( this, @@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment() { companion object { fun newInstance() = SearchAccountsFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 83eb30910..32475c78c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -17,11 +17,11 @@ import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.components.search.SearchViewModel +import com.keylesspalace.tusky.databinding.FragmentSearchBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.fragment_search.* import javax.inject.Inject abstract class SearchFragment : Fragment(R.layout.fragment_search), @@ -32,6 +32,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } + protected val binding by viewBinding(FragmentSearchBinding::bind) + private var snackbarErrorRetry: Snackbar? = null abstract fun createAdapter(): PagedListAdapter @@ -48,8 +50,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), } private fun setupSwipeRefreshLayout() { - swipeRefreshLayout.setOnRefreshListener(this) - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } private fun subscribeObservables() { @@ -59,7 +61,7 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), networkStateRefresh.observe(viewLifecycleOwner) { - searchProgressBar.visible(it == NetworkState.LOADING) + binding.searchProgressBar.visible(it == NetworkState.LOADING) if (it.status == Status.FAILED) { showError() @@ -69,7 +71,7 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), networkState.observe(viewLifecycleOwner) { - progressBarBottom.visible(it == NetworkState.LOADING) + binding.progressBarBottom.visible(it == NetworkState.LOADING) if (it.status == Status.FAILED) { showError() @@ -82,24 +84,25 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), } private fun initAdapter() { - searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) - searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) + binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) + binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context) adapter = createAdapter() - searchRecyclerView.adapter = adapter - searchRecyclerView.setHasFixedSize(true) - (searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.searchRecyclerView.adapter = adapter + binding.searchRecyclerView.setHasFixedSize(true) + (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } private fun showNoData(isEmpty: Boolean) { - if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) - searchNoResultsText.show() - else - searchNoResultsText.hide() + if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) { + binding.searchNoResultsText.show() + } else { + binding.searchNoResultsText.hide() + } } private fun showError() { if (snackbarErrorRetry?.isShown != true) { - snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry = null viewModel.retryAllSearches() @@ -122,8 +125,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), override fun onRefresh() { // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. - swipeRefreshLayout.post { - swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.post { + binding.swipeRefreshLayout.isRefreshing = false } viewModel.retryAllSearches() } 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 6d96bb5aa..f2ea85c0d 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 @@ -63,7 +63,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.fragment_search.* class SearchStatusesFragment : SearchFragment>(), StatusActionListener { @@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment, *> { - val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val statusDisplayOptions = StatusDisplayOptions( animateAvatars = preferences.getBoolean("animateGifAvatars", false), mediaPreviewEnabled = viewModel.mediaPreviewEnabled, @@ -91,12 +90,11 @@ class SearchStatusesFragment : SearchFragment FollowRequestsAdapter(this, animateAvatar, animateEmojis) else -> FollowAdapter(this, animateAvatar, animateEmojis) } - recyclerView.adapter = adapter + binding.recyclerView.adapter = adapter scrollListener = object : EndlessOnScrollListener(layoutManager) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { @@ -101,7 +104,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } } - recyclerView.addOnScrollListener(scrollListener) + binding.recyclerView.addOnScrollListener(scrollListener) fetchAccounts() } @@ -136,7 +139,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct val unmutedUser = mutesAdapter.removeItem(position) if (unmutedUser != null) { - Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { mutesAdapter.addItem(unmutedUser, position) onMute(true, id, position, notifications) @@ -180,7 +183,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct val unblockedUser = blocksAdapter.removeItem(position) if (unblockedUser != null) { - Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { blocksAdapter.addItem(unblockedUser, position) onBlock(true, id, position) @@ -260,7 +263,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct fetching = true if (fromId != null) { - recyclerView.post { adapter.setBottomLoading(true) } + binding.recyclerView.post { adapter.setBottomLoading(true) } } getFetchCallByListType(fromId) @@ -303,14 +306,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct fetching = false if (adapter.itemCount == 0) { - messageView.show() - messageView.setup( + binding.messageView.show() + binding.messageView.setup( R.drawable.elephant_friend_empty, R.string.message_empty, null ) } else { - messageView.hide() + binding.messageView.hide() } } @@ -339,15 +342,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct Log.e(TAG, "Fetch failure", throwable) if (adapter.itemCount == 0) { - messageView.show() + binding.messageView.show() if (throwable is IOException) { - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.hide() this.fetchAccounts(null) } } else { - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.hide() this.fetchAccounts(null) } } @@ -368,5 +371,4 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } } } - } 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 b85a87f31..f66791920 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status @@ -39,13 +40,13 @@ 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.util.viewBinding 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.Response import java.io.IOException import java.util.* @@ -58,49 +59,36 @@ import javax.inject.Inject */ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { - companion object { - @JvmStatic - fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { - val fragment = AccountMediaFragment() - val args = Bundle() - args.putString(ACCOUNT_ID_ARG, accountId) - args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh) - fragment.arguments = args - return fragment - } - - private const val ACCOUNT_ID_ARG = "account_id" - private const val TAG = "AccountMediaFragment" - private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" - } - - private var isSwipeToRefreshEnabled: Boolean = true - private var needToRefresh = false @Inject lateinit var api: MastodonApi + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private lateinit var accountId: String + private val adapter = MediaGridAdapter() private val statuses = mutableListOf() private var fetchingStatus = FetchingStatus.NOT_FETCHING - private lateinit var accountId: String + private var isSwipeToRefreshEnabled: Boolean = true + private var needToRefresh = false private val callback = object : SingleObserver>> { override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { - swipeRefreshLayout.isRefreshing = false - progressBar.visibility = View.GONE - topProgressBar?.hide() - statusView.show() + binding.swipeRefreshLayout.isRefreshing = false + binding.progressBar.visibility = View.GONE + binding.topProgressBar.hide() + binding.statusView.show() if (t is IOException) { - statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { doInitialLoadingIfNeeded() } } else { - statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { doInitialLoadingIfNeeded() } } @@ -112,9 +100,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { - swipeRefreshLayout.isRefreshing = false - progressBar.visibility = View.GONE - topProgressBar?.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.progressBar.visibility = View.GONE + binding.topProgressBar.hide() val body = response.body() body?.let { fetched -> @@ -126,11 +114,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } adapter.addTop(result) if (result.isNotEmpty()) - recyclerView.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) if (statuses.isEmpty()) { - statusView.show() - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) } } } @@ -181,18 +169,18 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) - recyclerView.layoutManager = layoutManager - recyclerView.adapter = adapter + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.adapter = adapter if (isSwipeToRefreshEnabled) { - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { refresh() } - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } - statusView.visibility = View.GONE + binding.statusView.visibility = View.GONE - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) { if (dy > 0) { @@ -216,7 +204,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } private fun refresh() { - statusView.hide() + binding.statusView.hide() if (fetchingStatus != FetchingStatus.NOT_FETCHING) return if (statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING @@ -229,12 +217,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr .subscribe(callback) if (!isSwipeToRefreshEnabled) - topProgressBar?.show() + binding.topProgressBar.show() } private fun doInitialLoadingIfNeeded() { if (isAdded) { - statusView.hide() + binding.statusView.hide() } if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING @@ -344,4 +332,19 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr needToRefresh = true } + companion object { + @JvmStatic + fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { + val fragment = AccountMediaFragment() + val args = Bundle() + args.putString(ACCOUNT_ID_ARG, accountId) + args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh) + fragment.arguments = args + return fragment + } + + private const val ACCOUNT_ID_ARG = "account_id" + private const val TAG = "AccountMediaFragment" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index fc4dfcb92..c68cfb5f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -32,13 +32,12 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.github.chrisbanes.photoview.PhotoViewAttacher -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentViewImageBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible import io.reactivex.subjects.BehaviorSubject -import kotlinx.android.synthetic.main.activity_view_media.* -import kotlinx.android.synthetic.main.fragment_view_image.* import kotlin.math.abs class ViewImageFragment : ViewMediaFragment() { @@ -48,6 +47,9 @@ class ViewImageFragment : ViewMediaFragment() { fun onPhotoTap() } + private var _binding: FragmentViewImageBinding? = null + private val binding get() = _binding!! + private lateinit var attacher: PhotoViewAttacher private lateinit var photoActionsListener: PhotoActionsListener private lateinit var toolbar: View @@ -71,18 +73,19 @@ class ViewImageFragment : ViewMediaFragment() { description: String?, showingDescription: Boolean ) { - photoView.transitionName = url - mediaDescription.text = description - captionSheet.visible(showingDescription) + binding.photoView.transitionName = url + binding.mediaDescription.text = description + binding.captionSheet.visible(showingDescription) startedTransition = false - loadImageFromNetwork(url, previewUrl, photoView) + loadImageFromNetwork(url, previewUrl, binding.photoView) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - toolbar = requireActivity().toolbar + toolbar = (requireActivity() as ViewMediaActivity).toolbar this.transition = BehaviorSubject.create() - return inflater.inflate(R.layout.fragment_view_image, container, false) + _binding = FragmentViewImageBinding.inflate(inflater, container, false) + return binding.root } @SuppressLint("ClickableViewAccessibility") @@ -105,7 +108,7 @@ class ViewImageFragment : ViewMediaFragment() { } } - attacher = PhotoViewAttacher(photoView).apply { + attacher = PhotoViewAttacher(binding.photoView).apply { // This prevents conflicts with ViewPager setAllowParentInterceptOnEdge(true) @@ -127,7 +130,7 @@ class ViewImageFragment : ViewMediaFragment() { var lastY = 0f - photoView.setOnTouchListener { v, event -> + binding.photoView.setOnTouchListener { v, event -> // This part is for scaling/translating on vertical move. // We use raw coordinates to get the correct ones during scaling @@ -140,11 +143,11 @@ class ViewImageFragment : ViewMediaFragment() { val diff = event.rawY - lastY // This code is to prevent transformations during page scrolling // If we are already translating or we reached the threshold, then transform. - if (photoView.translationY != 0f || abs(diff) > 40) { - photoView.translationY += (diff) - val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) - photoView.scaleY = scale - photoView.scaleX = scale + if (binding.photoView.translationY != 0f || abs(diff) > 40) { + binding.photoView.translationY += (diff) + val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) + binding.photoView.scaleY = scale + binding.photoView.scaleX = scale lastY = event.rawY return@setOnTouchListener true } @@ -158,13 +161,13 @@ class ViewImageFragment : ViewMediaFragment() { } private fun onGestureEnd() { - if (photoView == null) { + if (_binding == null) { return } - if (abs(photoView.translationY) > 180) { + if (abs(binding.photoView.translationY) > 180) { photoActionsListener.onDismiss() } else { - photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + binding.photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() } } @@ -173,15 +176,17 @@ class ViewImageFragment : ViewMediaFragment() { } override fun onToolbarVisibilityChange(visible: Boolean) { - if (photoView == null || !userVisibleHint || captionSheet == null) { + if (_binding == null || !userVisibleHint ) { return } isDescriptionVisible = showingDescription && visible val alpha = if (isDescriptionVisible) 1.0f else 0.0f - captionSheet.animate().alpha(alpha) + binding.captionSheet.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - captionSheet?.visible(isDescriptionVisible) + if (_binding != null) { + binding.captionSheet.visible(isDescriptionVisible) + } animation.removeListener(this) } }) @@ -189,8 +194,9 @@ class ViewImageFragment : ViewMediaFragment() { } override fun onDestroyView() { - Glide.with(this).clear(photoView) + Glide.with(this).clear(binding.photoView) transition.onComplete() + _binding = null super.onDestroyView() } @@ -253,7 +259,7 @@ class ViewImageFragment : ViewMediaFragment() { photoActionsListener.onBringUp() } // Hide progress bar only on fail request from internet - if (!isCacheRequest) progressBar?.hide() + if (!isCacheRequest && _binding != null) binding.progressBar.hide() // We don't want to overwrite preview with null when main image fails to load return !isCacheRequest } @@ -261,14 +267,16 @@ class ViewImageFragment : ViewMediaFragment() { @SuppressLint("CheckResult") override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { - progressBar?.hide() // Always hide the progress bar on success + if (_binding != null) { + binding.progressBar.hide() // Always hide the progress bar on success + } if (!startedTransition || !shouldStartTransition) { // Set this right away so that we don't have to concurrent post() requests startedTransition = true // post() because load() replaces image with null. Sometimes after we set // the thumbnail. - photoView.post { + binding.photoView.post { target.onResourceReady(resource, null) if (shouldStartTransition) photoActionsListener.onBringUp() } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 33d8f192c..a0912837d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -26,16 +26,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.MediaController -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView -import kotlinx.android.synthetic.main.activity_view_media.* -import kotlinx.android.synthetic.main.fragment_view_video.* class ViewVideoFragment : ViewMediaFragment() { + + private var _binding: FragmentViewVideoBinding? = null + private val binding get() = _binding!! + private lateinit var toolbar: View private val handler = Handler(Looper.getMainLooper()) private val hideToolbar = Runnable { @@ -52,7 +54,7 @@ class ViewVideoFragment : ViewMediaFragment() { override fun setUserVisibleHint(isVisibleToUser: Boolean) { // Start/pause/resume video playback as fragment is shown/hidden super.setUserVisibleHint(isVisibleToUser) - if (videoView == null) { + if (_binding == null) { return } @@ -60,10 +62,10 @@ class ViewVideoFragment : ViewMediaFragment() { if (mediaActivity.isToolbarVisible) { handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) } - videoView.start() + binding.videoView.start() } else { handler.removeCallbacks(hideToolbar) - videoView.pause() + binding.videoView.pause() mediaController.hide() } } @@ -75,11 +77,11 @@ class ViewVideoFragment : ViewMediaFragment() { description: String?, showingDescription: Boolean ) { - mediaDescription.text = description - mediaDescription.visible(showingDescription) + binding.mediaDescription.text = description + binding.mediaDescription.visible(showingDescription) - videoView.transitionName = url - videoView.setVideoPath(url) + binding.videoView.transitionName = url + binding.videoView.setVideoPath(url) mediaController = object : MediaController(mediaActivity) { override fun show(timeout: Int) { // We're doing manual auto-close management. @@ -100,10 +102,10 @@ class ViewVideoFragment : ViewMediaFragment() { } } - mediaController.setMediaPlayer(videoView) - videoView.setMediaController(mediaController) - videoView.requestFocus() - videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { + mediaController.setMediaPlayer(binding.videoView) + binding.videoView.setMediaController(mediaController) + binding.videoView.requestFocus() + binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { override fun onPause() { handler.removeCallbacks(hideToolbar) } @@ -117,31 +119,31 @@ class ViewVideoFragment : ViewMediaFragment() { } } }) - videoView.setOnPreparedListener { mp -> - val containerWidth = videoContainer.measuredWidth.toFloat() - val containerHeight = videoContainer.measuredHeight.toFloat() + binding.videoView.setOnPreparedListener { mp -> + val containerWidth = binding.videoContainer.measuredWidth.toFloat() + val containerHeight = binding.videoContainer.measuredHeight.toFloat() val videoWidth = mp.videoWidth.toFloat() val videoHeight = mp.videoHeight.toFloat() if(containerWidth/containerHeight > videoWidth/videoHeight) { - videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT + binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT } else { - videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT - videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT } // Wait until the media is loaded before accepting taps as we don't want toolbar to // be hidden until then. - videoView.setOnTouchListener { _, _ -> + binding.videoView.setOnTouchListener { _, _ -> mediaActivity.onPhotoTap() false } - progressBar.hide() + binding.progressBar.hide() mp.isLooping = true if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { - videoView.start() + binding.videoView.start() } } @@ -155,9 +157,10 @@ class ViewVideoFragment : ViewMediaFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - toolbar = requireActivity().toolbar mediaActivity = activity as ViewMediaActivity - return inflater.inflate(R.layout.fragment_view_video, container, false) + toolbar = mediaActivity.toolbar + _binding = FragmentViewVideoBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -174,7 +177,7 @@ class ViewVideoFragment : ViewMediaFragment() { } override fun onToolbarVisibilityChange(visible: Boolean) { - if (videoView == null || mediaDescription == null || !userVisibleHint) { + if (_binding == null || !userVisibleHint) { return } @@ -182,20 +185,22 @@ class ViewVideoFragment : ViewMediaFragment() { val alpha = if (isDescriptionVisible) 1.0f else 0.0f if (isDescriptionVisible) { // If to be visible, need to make visible immediately and animate alpha - mediaDescription.alpha = 0.0f - mediaDescription.visible(isDescriptionVisible) + binding.mediaDescription.alpha = 0.0f + binding.mediaDescription.visible(isDescriptionVisible) } - mediaDescription.animate().alpha(alpha) + binding.mediaDescription.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - mediaDescription?.visible(isDescriptionVisible) + if (_binding != null) { + binding.mediaDescription.visible(isDescriptionVisible) + } animation.removeListener(this) } }) .start() - if (visible && videoView.isPlaying && !isAudio) { + if (visible && binding.videoView.isPlaying && !isAudio) { hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) } else { handler.removeCallbacks(hideToolbar) @@ -204,4 +209,9 @@ class ViewVideoFragment : ViewMediaFragment() { override fun onTransitionEnd() { } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt index 9558b03f9..5fa80fcd4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -1,15 +1,67 @@ package com.keylesspalace.tusky.util import android.view.LayoutInflater +import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty /** * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c */ inline fun AppCompatActivity.viewBinding( - crossinline bindingInflater: (LayoutInflater) -> T + crossinline bindingInflater: (LayoutInflater) -> T ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater(layoutInflater) -} \ No newline at end of file +} + +class FragmentViewBindingDelegate( + val fragment: Fragment, + val viewBindingFactory: (View) -> T +) : ReadOnlyProperty { + private var binding: T? = null + + init { + fragment.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observe( + fragment, + { t -> + t?.lifecycle?.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + } + ) + } + ) + } + } + ) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + val binding = binding + if (binding != null) { + return binding + } + + val lifecycle = fragment.viewLifecycleOwner.lifecycle + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") + } + + return viewBindingFactory(thisRef.requireView()).also { this@FragmentViewBindingDelegate.binding = it } + } +} + +fun Fragment.viewBinding(viewBindingFactory: (View) -> T) = + FragmentViewBindingDelegate(this, viewBindingFactory) diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 8c993755b..5a5953ceb 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index 79f7fdb37..d3e716d6b 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -32,12 +32,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" + app:layout_constrainedHeight="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" - app:layout_constrainedHeight="true" /> + tools:visibility="visible" /> + \ No newline at end of file From f293670c14302bbb871c7e24a1e7b85675efd484 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 21 Mar 2021 12:42:28 +0100 Subject: [PATCH 22/41] migrating to ViewBinding part 6: the final cleanup (#2117) --- app/build.gradle | 5 +- .../components/compose/ComposeActivity.kt | 4 +- .../report/adapter/StatusViewHolder.kt | 67 +++++++++---------- .../report/adapter/StatusesAdapter.kt | 22 +++--- .../com/keylesspalace/tusky/db/DraftEntity.kt | 2 +- .../keylesspalace/tusky/entity/Attachment.kt | 2 +- .../com/keylesspalace/tusky/entity/Emoji.kt | 2 +- .../keylesspalace/tusky/entity/NewStatus.kt | 2 +- .../tusky/service/SendTootService.kt | 2 +- .../tusky/viewdata/AttachmentViewData.kt | 2 +- build.gradle | 4 +- 11 files changed, 52 insertions(+), 62 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 583abb11d..dc240af0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' apply from: "../instance-build.gradle" @@ -64,9 +64,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - androidExtensions { - experimental = true - } buildFeatures { viewBinding 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 5c5b35328..649ed1420 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 @@ -76,10 +76,10 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException -import java.util.* +import java.util.Locale import javax.inject.Inject import kotlin.math.max import kotlin.math.min 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 8201de2e5..88a246f40 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 @@ -21,6 +21,7 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener @@ -28,16 +29,15 @@ import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER import com.keylesspalace.tusky.viewdata.toViewData -import kotlinx.android.synthetic.main.item_report_status.view.* import java.util.* class StatusViewHolder( - itemView: View, + private val binding: ItemReportStatusBinding, private val statusDisplayOptions: StatusDisplayOptions, private val viewState: StatusViewState, private val adapterHandler: AdapterHandler, private val getStatusForPosition: (Int) -> Status? -) : RecyclerView.ViewHolder(itemView) { +) : RecyclerView.ViewHolder(binding.root) { private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) @@ -56,16 +56,16 @@ class StatusViewHolder( } init { - itemView.statusSelection.setOnCheckedChangeListener { _, isChecked -> + binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> status()?.let { status -> adapterHandler.setStatusChecked(status, isChecked) } } - itemView.status_media_preview_container.clipToOutline = true + binding.statusMediaPreviewContainer.clipToOutline = true } fun bind(status: Status) { - itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) updateTextView() @@ -86,18 +86,18 @@ class StatusViewHolder( if (status.spoilerText.isBlank()) { setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) - itemView.statusContentWarningButton.hide() - itemView.statusContentWarningDescription.hide() + binding.statusContentWarningButton.hide() + binding.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis) - itemView.statusContentWarningDescription.text = emojiSpoiler - itemView.statusContentWarningDescription.show() - itemView.statusContentWarningButton.show() + val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) + binding.statusContentWarningDescription.text = emojiSpoiler + binding.statusContentWarningDescription.show() + binding.statusContentWarningButton.show() setContentWarningButtonText(viewState.isContentShow(status.id, true)) - itemView.statusContentWarningButton.setOnClickListener { + binding.statusContentWarningButton.setOnClickListener { status()?.let { status -> val contentShown = viewState.isContentShow(status.id, true) - itemView.statusContentWarningDescription.invalidate() + binding.statusContentWarningDescription.invalidate() viewState.setContentShow(status.id, !contentShown) setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) @@ -110,9 +110,9 @@ class StatusViewHolder( private fun setContentWarningButtonText(contentShown: Boolean) { if(contentShown) { - itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_less) + binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less) } else { - itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_more) + binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more) } } @@ -122,26 +122,26 @@ class StatusViewHolder( emojis: List, listener: LinkListener) { if (expanded) { - val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis) - LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) + val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis) + LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener) } else { - LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) + LinkHelper.setClickableMentions(binding.statusContent, mentions, listener) } - if (itemView.statusContent.text.isNullOrBlank()) { - itemView.statusContent.hide() + if (binding.statusContent.text.isNullOrBlank()) { + binding.statusContent.hide() } else { - itemView.statusContent.show() + binding.statusContent.show() } } private fun setCreatedAt(createdAt: Date?) { if (statusDisplayOptions.useAbsoluteTime) { - itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) + binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) } else { - itemView.timestampInfo.text = if (createdAt != null) { + binding.timestampInfo.text = if (createdAt != null) { val then = createdAt.time val now = System.currentTimeMillis() - TimestampUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now) + TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now) } else { // unknown minutes~ "?m" @@ -149,28 +149,27 @@ class StatusViewHolder( } } - private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) { /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { - itemView.buttonToggleContent.setOnClickListener{ + binding.buttonToggleContent.setOnClickListener{ status()?.let { status -> viewState.setCollapsed(status.id, !collapsed) updateTextView() } } - itemView.buttonToggleContent.show() + binding.buttonToggleContent.show() if (collapsed) { - itemView.buttonToggleContent.setText(R.string.status_content_show_more) - itemView.statusContent.filters = COLLAPSE_INPUT_FILTER + binding.buttonToggleContent.setText(R.string.status_content_show_more) + binding.statusContent.filters = COLLAPSE_INPUT_FILTER } else { - itemView.buttonToggleContent.setText(R.string.status_content_show_less) - itemView.statusContent.filters = NO_INPUT_FILTER + binding.buttonToggleContent.setText(R.string.status_content_show_less) + binding.statusContent.filters = NO_INPUT_FILTER } } else { - itemView.buttonToggleContent.hide() - itemView.statusContent.filters = NO_INPUT_FILTER + binding.buttonToggleContent.hide() + binding.statusContent.filters = NO_INPUT_FILTER } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index 34817ca0d..b66ac4f3c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -20,8 +20,8 @@ import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -29,29 +29,25 @@ class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagedListAdapter(STATUS_COMPARATOR) { +) : PagedListAdapter(STATUS_COMPARATOR) { private val statusForPosition: (Int) -> Status? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_report_status, parent, false) - return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler, + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { + val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler, statusForPosition) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { getItem(position)?.let { status -> - (holder as? StatusViewHolder)?.bind(status) + holder.bind(status) } - } companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = oldItem == newItem @@ -59,7 +55,5 @@ class StatusesAdapter( override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = oldItem.id == newItem.id } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index be1eca589..184ff2c30 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -23,7 +23,7 @@ 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 +import kotlinx.parcelize.Parcelize @Entity @TypeConverters(Converters::class) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt index 1de7bd787..3e14519ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt @@ -22,7 +22,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonParseException import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class Attachment( diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt index fe7a22c73..42bb99e93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class Emoji( diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt index ebc979f36..16cbc6a7c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize data class NewStatus( val status: String, 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 b54a941b1..c6b07bb72 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -28,7 +28,7 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.SaveTootHelper import dagger.android.AndroidInjection -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import retrofit2.Call import retrofit2.Callback import retrofit2.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index 52eb31ac5..a7b2bffc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -3,7 +3,7 @@ package com.keylesspalace.tusky.viewdata import android.os.Parcelable import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class AttachmentViewData( diff --git a/build.gradle b/build.gradle index 75d0217a4..d6ef9eec0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '1.4.31' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 496245c6973d02c3f2c0ddd5aca2796a28496fd3 Mon Sep 17 00:00:00 2001 From: Connyduck Date: Thu, 25 Mar 2021 15:00:27 +0000 Subject: [PATCH 23/41] Added translation using Weblate (Sinhala) --- app/src/main/res/values-si/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-si/strings.xml diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-si/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 339bd8fd92e84ca0534eb15e5c3ee3b4039d7b7b Mon Sep 17 00:00:00 2001 From: Deleted User Date: Thu, 25 Mar 2021 15:00:27 +0000 Subject: [PATCH 24/41] Translated using Weblate (German) Currently translated at 97.1% (446 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ --- app/src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bae3e4e30..ccacb88b1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -499,4 +499,5 @@ GIF-Emojis animieren Jemand, den ich abonniert habe, etwas Neues veröffentlicht %s hat gerade etwas gepostet + %dm \ No newline at end of file From 4cf6d516281f4a33f342f0b9999245888150b35a Mon Sep 17 00:00:00 2001 From: helabasa Date: Thu, 25 Mar 2021 15:00:27 +0000 Subject: [PATCH 25/41] Translated using Weblate (Sinhala) Currently translated at 3.0% (14 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/si/ --- app/src/main/res/values-si/strings.xml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index a6b3daec9..d9993b2f9 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -1,2 +1,17 @@ - \ No newline at end of file + + %1$s සහ %2$s + %1$s + පෙරහන + යොදන්න + ආපසු + ගිණුම් + විනාඩි 5 + විනාඩි 30 + හෝරා 6 + හෝරා 1 + දින 3 + දින 1 + දින 7 + සංස්කරණය + \ No newline at end of file From 32b7b203bb24910089270cd94abbc07aaa2697ca Mon Sep 17 00:00:00 2001 From: Porrumentzio Date: Thu, 25 Mar 2021 15:00:27 +0000 Subject: [PATCH 26/41] Translated using Weblate (Basque) Currently translated at 93.0% (427 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/eu/ --- app/src/main/res/values-eu/strings.xml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 15925a361..2430e0644 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -34,7 +34,7 @@ Profila editatu Zirriborroak Lizentziak - %s-(e)k bultzatu du + %s(e)k bultzatu du Kontuz edukiarekin Ezkutuko multimedia Sakatu ikusteko @@ -43,8 +43,8 @@ Zabaldu Bildu Edukirik ez. Arrastatu behera birkargatzeko! - %s-(e)k zure tuta bultzatu du - %s-(e)k zure tuta gogoko du + %s(e)k zure tuta bultzatu du + %s(e)k zure tuta gogoko du %s(e)k jarraitu zaitu \@%s salatu Informazio gehigarria? @@ -188,7 +188,7 @@ Bultzatutako tuten jakinarazpenak Gogokoak Zure tutak gogoko bezala ezartzerakoan jakinarazpenak - %s-(e)k aipatu zaitu + %s(e)k aipatu zaitu %1$s, %2$s, %3$s eta beste %4$d %1$s, %2$s eta %3$s %1$s eta %2$s @@ -228,7 +228,7 @@ Jarraitzen zaitu Eduki mingarria erakutsi Multimedia - \@%s-ri erantzuten + \@%s-(r)i erantzuten Gehiago erakutsi Gehitu kontua Mastodon kontua gehitu @@ -313,7 +313,7 @@ Media jaisten %s ez dago ezkutatua Tut hau ezabatu eta zirriborro berria egin\? - Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenentan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira. + Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira. Domeinu osoa ezkutatu Galdeketak bukatu dira Iragazkiak @@ -383,7 +383,7 @@ Garbitu Iragazi Aplikatu - Idatzi Toot-a + Idatzi tuta Idatzi Ziur zaude jakinarazpen guztiak betirako garbitu nahi dituzula\? %s irudiarentzako ekintzak @@ -456,7 +456,6 @@ Traolak Ez erakutsi jakinarazpenak Desmututu %s - Ezkutatu goiko tresna-barraren izenburua Erakutsi berrespen-abisua tuta bultzatu aurretik Erakutsi esteken aurrebista denbora-lerroetan @@ -470,7 +469,7 @@ Goia Nabigatze posizio nagusia Erakutsi gradiente koloretsua ezkutuko mediarentzako - jarraipena-eskaera + jarraipen-eskaera Desmututu elkarrizketa Desmututu %s Mututu %s(r)en jakinarazpenak From 62a5b7295e53c48ffe1477dee16c9d5c8018d359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Gera?= Date: Thu, 25 Mar 2021 15:00:27 +0000 Subject: [PATCH 27/41] Translated using Weblate (Hungarian) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ --- app/src/main/res/values-hu/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index dbb5c8663..175724baa 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -206,6 +206,7 @@ %1$s, %2$s meg %3$s %1$s és %2$s + %d új interakció %d új interakció Zárolt fiók @@ -276,6 +277,7 @@ Kedvencnek jelölte %1$s és %2$s + elérted a fülek maximális számát (%1$d) elérted a fülek maximális számát (%1$d) Nincs leírás @@ -491,6 +493,7 @@ 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. Nem tölthetsz fel %1$d médiacsatolmányból többet. Profilok mérőszámainak elrejtése @@ -514,4 +517,6 @@ %s épp tülkölt Jóllét Egyedi emojik animálása + Leiratkozás + Feliratkozás \ No newline at end of file From 41847bc009daa84d9d063edbece635235810890a Mon Sep 17 00:00:00 2001 From: Zero King Date: Tue, 6 Apr 2021 15:18:52 +0000 Subject: [PATCH 28/41] Add missing breaks in switch statements (#2127) --- .../com/keylesspalace/tusky/adapter/NotificationsAdapter.java | 1 + .../com/keylesspalace/tusky/fragment/NotificationsFragment.java | 2 ++ 2 files changed, 3 insertions(+) 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 cdc43741f..92ebbfe42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -235,6 +235,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); holder.setupActionListener(accountActionListener, concreteNotificaton.getAccount().getId()); } + break; } default: } 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 5db3983f6..a6a3e311e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -863,6 +863,7 @@ public class NotificationsFragment extends SFragment implements adapter.setMediaPreviewEnabled(enabled); fullyRefresh(); } + break; } case "showNotificationsFilter": { if (isAdded()) { @@ -870,6 +871,7 @@ public class NotificationsFragment extends SFragment implements updateFilterVisibility(); fullyRefreshWithProgressBar(true); } + break; } } } From 76aa52a81804b04b24b295500a82bb9320628978 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Tue, 6 Apr 2021 12:58:35 +0000 Subject: [PATCH 29/41] Translated using Weblate (Gaelic) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ Translated using Weblate (Gaelic) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ --- app/src/main/res/values-gd/strings.xml | 66 +++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 6d35b7af2..c9d0d5fc0 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -26,14 +26,14 @@ Freagair… Lorg… Clàraich a-steach le Mastodon - TÙT! + DÙD! Meur-chlàr Emoji Na lean tuilleadh Lean Barrachd Feuch ris a-rithist Dùin - TÙT + DÙD Sguab às Sguab às is dèan dreachd ùr air Dèan gearan @@ -55,13 +55,13 @@ Cuir crìoch air an fho-sgrìobhadh Fo-sgrìobh Beòthaich na h-Emojis gnàthaichte - Bha againn ris an tùt a bha thu airson freagairt dha a thoirt air falbh + Bha againn ris an dùd a bha thu airson freagairt dha a thoirt air falbh Chaidh an dreach a sguabadh às Cha deach leinn fiosrachadh na freagairte a luchdadh Seann-dreachdan Chaidh dealbhadh gu tur ùr a chur air gleus nan dreachdan aig Tusky ach am biodh e nas luaithe, nas fhasa cleachdadh is nas lugha de bhugaichean ann. \n Gheibh thu grèim air na seann-dreachdan agad fhathast le putan air sgrìn ùr nan dreachdan ach thèid an toirt air falbh le ùrachadh ri teachd! - Cha b’ urrainn dhuinn an tùt a chur! + Cha b’ urrainn dhuinn an dùd a chur! Ceanglachain Fuaim A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? @@ -86,15 +86,15 @@ Thoir sùil air na brathan Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: \n -\n - Brathan air annsachdan/brosnachaidhean/leanntainn -\n - Cunntas nan annsachdan/brosnachaidhean air tùtaidhean +\n - Brathan air annsachdan/brosnachaidhean/leantainn +\n - Cunntas nan annsachdan/brosnachaidhean air dùdan \n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean \n \n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh. Slàinte-inntinn - Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh tùt ùr - Tùtaidhean ùra - dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh tùt ùr + Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr + Dùdan ùra + dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr Tha %s air rud a phostadh Chan eil brath-fios ann. Brathan-fios @@ -172,7 +172,7 @@ %1$s • %2$s Gnìomhan dhan dealbh %s A bheil thu cinnteach gu bheil thu airson na brathan uile agad fhalamhachadh gu buan\? - Sgrìobh tùt + Sgrìobh dùd Cuir an sàs Criathraich Falamhaich @@ -236,7 +236,7 @@ Uaireigin eile Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs Feumaidh tu an aplacaid ath-thòiseachadh - Fosgail an tùt + Fosgail an dùd Leudaich/Co-theannaich gach staid ’Ga lorg… Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach @@ -244,11 +244,11 @@ Stoidhle nan Emojis Chaidh lethbhreac dheth a chur air an stòr-bhòrd Chan eil Emojis gnàthaichte aig an ionstans %s agad - Chaidh lethbhreac dhen tùt agad a shàbhaladh ’na dhreachd + Chaidh lethbhreac dhen dùd agad a shàbhaladh ’na dhreachd Chaidh sgur dhen chur - A’ cur nan tùt - Mearachd a’ cur an tùt - A’ cur an tùt… + A’ cur nan dùd + Mearachd a’ cur an dùid + A’ cur an dùid… A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas @@ -273,14 +273,14 @@ Cuir cunntas ris An abairt ri chriathradh Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin - Leudaich tùtaidhean le rabhaidhean susbainte an-còmhnaidh - Co-roinn ceangal dhan tùt - Co-roinn susbaint an tùt + Leudaich dùdan ris a bheil rabhadh susbainte an-còmhnaidh + Co-roinn ceangal dhan dùd + Co-roinn susbaint an dùid ’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html - Brathan nuair a thèid tùt agad a chomharrachadh ’na annsachd - Brathan nuair a thèid tùt agad brosnachadh - A bheil thu airson an tùt seo a sguabadh às is dreachd ùr a dhèanamh air\? - A bheil thu airson an tùt seo a sguabadh às\? + Brathan nuair a thèid dùd agad a chomharrachadh ’na annsachd + Brathan nuair a thèid dùd agad brosnachadh + A bheil thu airson an dùd seo a sguabadh às is dreachd ùr a dhèanamh air\? + A bheil thu airson an dùd seo a sguabadh às\? ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! \n \nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. @@ -288,16 +288,16 @@ \n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. \n \nGheibh thu barrachd fiosrachaidh air joinmastodon.org. - Co-roinn an tùt le… - Co-roinn URL an tùt le… - Cuir tùt air an sgeideal - Faicsinneachd an tùt - Tùtaidhean air an sgeideal - Chuir %s an tùt agad ris na h-annsachdan - Bhrosnaich %s an tùt agad - Tùtaidhean air an sgeideal - Tùt - Mearachd a’ cur an tùt. + Co-roinn an dùd le… + Co-roinn URL an dùid le… + Cuir dùd air an sgeideal + Faicsinneachd an dùid + Dùdan air an sgeideal + Chuir %s an dùd agad ris na h-annsachdan + Bhrosnaich %s an dùd agad + Dùdan air an sgeideal + Dùd + Mearachd a’ cur an dùd. Dì-mhùch %s Tagaichean hais Luchd-leantainn @@ -510,7 +510,7 @@ Postaichean Tabaichean Teachdaireachdan dìreach - Co-nasgaichte + Co-naisgte Ionadail Dachaigh Dh’fhàillig leis an luchdadh suas. From 160a24135a752ae9b08e78b7a3f91a38a9472696 Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Tue, 6 Apr 2021 12:58:36 +0000 Subject: [PATCH 30/41] Translated using Weblate (Vietnamese) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7f41d963e..d5aba7d5b 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -141,8 +141,8 @@ Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Đã lưu - Lượt thích + Xem tút đã lưu + Thích Trang cá nhân Đóng Thử lại @@ -186,7 +186,7 @@ Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Đã lưu + Lưu Người theo dõi Theo dõi Ghim @@ -196,11 +196,11 @@ Xếp tab Tin nhắn Thế giới - Cộng đồng + Máy chủ Thông báo Bảng tin Nháp - Lượt thích + Thích Máy chủ là gì\? Tải xem trước hình ảnh Hiện lượt trả lời @@ -351,8 +351,8 @@ %d ngày nữa kết thúc - 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 + Cuộc bình chọn của bạn đã kết thúc + Cuộc bình chọn đã kết thúc Bình chọn xong kết thúc lúc %s From f111081f584bf556f4bb9e2dc03f1371a7775272 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 6 Apr 2021 12:58:36 +0000 Subject: [PATCH 31/41] Translated using Weblate (Persian) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ --- app/src/main/res/values-fa/strings.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index fc268acf7..5c2207a13 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -500,4 +500,21 @@ اموجی‌های شخصی متحرّک کسی که مشترکش شده‌ام، بوقی جدید منتشر کرد %s چیزی فرستاد + بوقی که پاسخی به آن را پیش‌نویس کردید، برداشته شده + شکست در بار کردن اطّلاعات پاسخ + برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون: +\n +\n - آگاهی‌های برگزیدن، تقویت و پی‌گیری +\n - شمار برگزیدن و تقویت بوق‌ها +\n - آمار پی‌گیر و فرسته روی نمایه‌ها +\n +\n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. + ویژگی پیش‌نویس در تاسکی به صورت کامل بازطرّاحی شده تا سریع‌تر، کاربرپسندتر و کم‌مشکل‌تر باشد. +\n همجنان می‌توانید از طریق دکمه‌ای دز صفحهٔ پیش‌نویس‌های جدید، به پیش‌نویس‌های قدیمیتان دسترسی داشته باشید، ولی در به‌روز رسانی آینده برداشته خواهند شد! + واقعاً می‌خواهید فهرست %s را حذف کنید؟ + + نمی‌توانید بیش از %1$d رسانه بارگذارید. + نمی‌توانید بیش از %1$d رسانه بارگذارید. + + نامعیّن \ No newline at end of file From 4be6bc5fb3ebe7efc8be5fa6e4fc023f36570066 Mon Sep 17 00:00:00 2001 From: XoseM Date: Tue, 6 Apr 2021 13:07:36 +0000 Subject: [PATCH 32/41] Translated using Weblate (Galician) Currently translated at 100.0% (12 of 12 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/ --- fastlane/metadata/android/gl/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/gl/full_description.txt b/fastlane/metadata/android/gl/full_description.txt index 2f126b38e..53a8794f2 100644 --- a/fastlane/metadata/android/gl/full_description.txt +++ b/fastlane/metadata/android/gl/full_description.txt @@ -3,7 +3,7 @@ Tusky é un cliente lixeiro para Mastodon, o servidor para redes sociais libres • Material Design • Maioría das APIs de Mastodon implementadas • Soporte multi-conta. -• Decorado escuro e claro coa posibilidade de cambio automático según a hora do día +• Decorado escuro e claro coa posibilidade de cambio automático según a hora do día • Borradores - compoñer toots e gardalos para máis tarde • Escoller entre varios estilos de emoji • Optimizado para tódolos tamaños de pantalla From 43c38ce2091d95ed8fd06bd492c5603170d09c2f Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 6 Apr 2021 13:07:36 +0000 Subject: [PATCH 33/41] Translated using Weblate (Persian) Currently translated at 100.0% (12 of 12 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/ --- fastlane/metadata/android/fa/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/fa/changelogs/80.txt | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 fastlane/metadata/android/fa/changelogs/77.txt create mode 100644 fastlane/metadata/android/fa/changelogs/80.txt diff --git a/fastlane/metadata/android/fa/changelogs/77.txt b/fastlane/metadata/android/fa/changelogs/77.txt new file mode 100644 index 000000000..27f9a712d --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/77.txt @@ -0,0 +1,10 @@ +تاسکی نگارش ۱۳٫۰ + +- پشتیبانی از یادداشت‌های نمایه (ویژگی ماستودون ۳٫۲٫۰) +- پشتیبانی از اعلامیه‌های مدیر (ویژگی ماستودون ۳٫۱٫۰) + +- آواتار حساب گزیده‌تان در نوار ابزار اصلی نشان داده می‌شود +- زدن روی نام نمایشی در خط زمانی، نمایهٔ آن کاربر را می‌گشاید + +- کلّی رفع اشکال و بهبودهای جزیی +- بازگردانی‌های بهبودیافته diff --git a/fastlane/metadata/android/fa/changelogs/80.txt b/fastlane/metadata/android/fa/changelogs/80.txt new file mode 100644 index 000000000..f8e0c39d4 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/80.txt @@ -0,0 +1,7 @@ +تاسکی نگارش ۱۴٫۰ + +- هنگامی که کاربر پی‌گرفته‌ای بوق می‌زند، آگاه شوید - نقشک زنگ روی نمایه‌اش را بزنید! (ویژگی ماستودون ۳٫۳٫۰) +- ویژگی پیش‌نویس در تاسکی برای سریع‌تر، کاربرپسندتر و کم‌مشکل‌تر بودن، به کلّی باز طرّاحی شده. +- حالت سلامتی جدیدی افزوده شده ه می‌گذارد ویژگی‌های خاصی را در تاسکی محدود کنید. +- تاسکی اکنون می‌تواند پویانمایی اموجی‌های شخصی را نشان دهد. +گزارش تغییر کامل: https://github.com/tuskyapp/Tusky/releases From dee6a3a16033a6e651fdf6d82acfcf5fca535521 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 10 Apr 2021 20:30:44 +0200 Subject: [PATCH 34/41] always show follow requests in main menu (#1809) * always show follow requests in main menu * update recyclerview to v1.2.0 * fix bug that shows follow requests info to wrong users --- app/build.gradle | 2 +- .../tusky/AccountListActivity.kt | 8 +++- .../tusky/AccountsInListFragment.kt | 4 +- .../com/keylesspalace/tusky/ListsActivity.kt | 4 +- .../com/keylesspalace/tusky/MainActivity.kt | 29 ++++++-------- .../tusky/TabPreferenceActivity.kt | 10 ++--- .../tusky/adapter/AccountFieldEditAdapter.kt | 4 +- .../tusky/adapter/BlocksAdapter.java | 2 +- .../tusky/adapter/FollowRequestViewHolder.kt | 4 +- .../adapter/FollowRequestsHeaderAdapter.kt | 40 +++++++++++++++++++ .../tusky/adapter/MutesAdapter.java | 4 +- .../tusky/adapter/NotificationsAdapter.java | 6 +-- .../tusky/adapter/PlaceholderViewHolder.java | 2 +- .../tusky/adapter/PollAdapter.kt | 4 +- .../tusky/adapter/SavedTootAdapter.java | 4 +- .../tusky/adapter/StatusBaseViewHolder.java | 32 +++++++-------- .../adapter/StatusDetailedViewHolder.java | 4 +- .../tusky/adapter/StatusViewHolder.java | 4 +- .../keylesspalace/tusky/adapter/TabAdapter.kt | 6 +-- .../components/compose/MediaPreviewAdapter.kt | 2 +- .../conversation/ConversationViewHolder.java | 2 +- .../tusky/components/drafts/DraftsAdapter.kt | 2 +- .../adapter/DomainMutesAdapter.kt | 2 +- .../report/adapter/StatusViewHolder.kt | 2 +- .../tusky/fragment/AccountListFragment.kt | 19 +++++++-- .../tusky/fragment/AccountMediaFragment.kt | 2 +- .../layout/item_follow_requests_header.xml | 12 ++++++ app/src/main/res/values/strings.xml | 3 ++ 28 files changed, 143 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt create mode 100644 app/src/main/res/layout/item_follow_requests_header.xml diff --git a/app/build.gradle b/app/build.gradle index dc240af0e..cf1137308 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,7 +113,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.3.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.recyclerview:recyclerview:1.2.0" implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.preference:preference-ktx:1.1.1" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index 71118501c..7f00150f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -46,6 +46,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { val type = intent.getSerializableExtra(EXTRA_TYPE) as Type val id: String? = intent.getStringExtra(EXTRA_ID) + val accountLocked: Boolean = intent.getBooleanExtra(EXTRA_ACCOUNT_LOCKED, false) setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { @@ -64,7 +65,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { supportFragmentManager .beginTransaction() - .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) + .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked)) .commit() } @@ -73,12 +74,15 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { companion object { private const val EXTRA_TYPE = "type" private const val EXTRA_ID = "id" + private const val EXTRA_ACCOUNT_LOCKED = "acc_locked" @JvmStatic - fun newIntent(context: Context, type: Type, id: String? = null): Intent { + @JvmOverloads + fun newIntent(context: Context, type: Type, id: String? = null, accountLocked: Boolean = false): Intent { return Intent(context, AccountListActivity::class.java).apply { putExtra(EXTRA_TYPE, type) putExtra(EXTRA_ID, id) + putExtra(EXTRA_ACCOUNT_LOCKED, accountLocked) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 0151e070c..df381aa17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -181,7 +181,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { binding.notificationTextView.hide() binding.acceptButton.hide() binding.rejectButton.setOnClickListener { - onRemoveFromList(getItem(holder.adapterPosition).id) + onRemoveFromList(getItem(holder.bindingAdapterPosition).id) } binding.rejectButton.contentDescription = binding.root.context.getString(R.string.action_remove_from_list) @@ -217,7 +217,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { binding.notificationTextView.hide() binding.acceptButton.hide() binding.rejectButton.setOnClickListener { - val (account, inAList) = getItem(holder.adapterPosition) + val (account, inAList) = getItem(holder.bindingAdapterPosition) if (inAList) { onRemoveFromList(account.id) } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 04311a660..be995e9ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -250,9 +250,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { override fun onClick(v: View) { if (v == itemView) { - onListSelected(getItem(adapterPosition).id) + onListSelected(getItem(bindingAdapterPosition).id) } else { - onMore(getItem(adapterPosition), v) + onMore(getItem(bindingAdapterPosition), v) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index d46d3d560..1628d41d9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -121,6 +121,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private lateinit var glide: RequestManager + private var accountLocked: Boolean = false + private val emojiInitCallback = object : InitCallback() { override fun onInitialized() { if (!isDestroyed) { @@ -399,6 +401,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje startActivityWithSlideInAnimation(intent) } }, + primaryDrawerItem { + nameRes = R.string.action_view_follow_requests + iconicsIcon = GoogleMaterial.Icon.gmd_person_add + onClick = { + val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked) + startActivityWithSlideInAnimation(intent) + } + }, primaryDrawerItem { nameRes = R.string.action_lists iconicsIcon = GoogleMaterial.Icon.gmd_list @@ -660,22 +670,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje accountManager.updateActiveAccount(me) NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) - // Show follow requests in the menu, if this is a locked account. - if (me.locked && binding.mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) { - val followRequestsItem = primaryDrawerItem { - identifier = DRAWER_ITEM_FOLLOW_REQUESTS - nameRes = R.string.action_view_follow_requests - iconicsIcon = GoogleMaterial.Icon.gmd_person_add - onClick = { - val intent = Intent(this@MainActivity, AccountListActivity::class.java) - intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS) - startActivityWithSlideInAnimation(intent) - } - } - binding.mainDrawer.addItemAtPosition(4, followRequestsItem) - } else if (!me.locked) { - binding.mainDrawer.removeItems(DRAWER_ITEM_FOLLOW_REQUESTS) - } + accountLocked = me.locked + updateProfiles() updateShortcut(this, accountManager.activeAccount!!) } @@ -789,7 +785,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje companion object { private const val TAG = "MainActivity" // logging tag private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 - private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 const val STATUS_URL = "statusUrl" } diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 413e754bf..67cd9cb61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -109,17 +109,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - val temp = currentTabs[viewHolder.adapterPosition] - currentTabs[viewHolder.adapterPosition] = currentTabs[target.adapterPosition] - currentTabs[target.adapterPosition] = temp + val temp = currentTabs[viewHolder.bindingAdapterPosition] + currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition] + currentTabs[target.bindingAdapterPosition] = temp - currentTabsAdapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition) + currentTabsAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) saveTabs() return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - onTabRemoved(viewHolder.adapterPosition) + onTabRemoved(viewHolder.bindingAdapterPosition) } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index 29cbec285..f7f4553a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -65,7 +65,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onBlock(false, id, position); } 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 f19dde822..a7e927433 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -51,13 +51,13 @@ class FollowRequestViewHolder( fun setupActionListener(listener: AccountActionListener, accountId: String) { binding.acceptButton.setOnClickListener { - val position = adapterPosition + val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { listener.onRespondToFollowRequest(true, accountId, position) } } binding.rejectButton.setOnClickListener { - val position = adapterPosition + val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { listener.onRespondToFollowRequest(false, accountId, position) } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt new file mode 100644 index 000000000..60ab40086 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt @@ -0,0 +1,40 @@ +/* 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.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R + +class FollowRequestsHeaderAdapter(private val instanceName: String, private val accountLocked: Boolean) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_follow_requests_header, parent, false) as TextView + return HeaderViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: HeaderViewHolder, position: Int) { + viewHolder.textView.text = viewHolder.textView.context.getString(R.string.follow_requests_info, instanceName) + } + + override fun getItemCount() = if (accountLocked) 0 else 1 + +} + +class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView) 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 20140fffd..f63af6ca6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -122,9 +122,9 @@ public class MutesAdapter extends AccountAdapter { } void setupActionListener(final AccountActionListener listener) { - unmute.setOnClickListener(v -> listener.onMute(false, id, getAdapterPosition(), false)); + unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false)); muteNotifications.setOnClickListener( - v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); + v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications)); itemView.setOnClickListener(v -> listener.onViewAccount(id)); } } 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 92ebbfe42..b2f12b81a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -541,8 +541,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } contentWarningButton.setOnClickListener(view -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getBindingAdapterPosition()); } statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE); }); @@ -619,7 +619,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION && notificationActionListener != null) { notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java index 2f946108f..f8f1a0b53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java @@ -41,7 +41,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { loadMoreButton.setEnabled(true); loadMoreButton.setOnClickListener(v -> { loadMoreButton.setEnabled(false); - listener.onLoadMore(getAdapterPosition()); + listener.onLoadMore(getBindingAdapterPosition()); }); } 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 fa69e3386..1f57cc4e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -100,7 +100,7 @@ class PollAdapter: RecyclerView.Adapter>() { radioButton.isChecked = option.selected radioButton.setOnClickListener { pollOptions.forEachIndexed { index, pollOption -> - pollOption.selected = index == holder.adapterPosition + pollOption.selected = index == holder.bindingAdapterPosition notifyItemChanged(index) } } @@ -110,7 +110,7 @@ class PollAdapter: RecyclerView.Adapter>() { checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) checkBox.isChecked = option.selected checkBox.setOnCheckedChangeListener { _, isChecked -> - pollOptions[holder.adapterPosition].selected = isChecked + pollOptions[holder.bindingAdapterPosition].selected = isChecked } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java index 6d4889c84..af9c31d5d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java @@ -113,9 +113,9 @@ public class SavedTootAdapter extends RecyclerView.Adapter { suppr.setOnClickListener(v -> { v.setEnabled(false); - handler.delete(getAdapterPosition(), item); + handler.delete(getBindingAdapterPosition(), item); }); - view.setOnClickListener(v -> handler.click(getAdapterPosition(), item)); + view.setOnClickListener(v -> handler.click(getBindingAdapterPosition(), item)); } } } 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 3fc27d7cd..d6cee6261 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -217,8 +217,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setContentWarningButtonText(expanded); contentWarningButton.setOnClickListener(view -> { contentWarningDescription.invalidate(); - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onExpandedChange(!expanded, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(!expanded, getBindingAdapterPosition()); } setContentWarningButtonText(!expanded); @@ -513,15 +513,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); sensitiveMediaShow.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(false, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(false, getBindingAdapterPosition()); } v.setVisibility(View.GONE); sensitiveMediaWarning.setVisibility(View.VISIBLE); }); sensitiveMediaWarning.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(true, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getBindingAdapterPosition()); } v.setVisibility(View.GONE); sensitiveMediaShow.setVisibility(View.VISIBLE); @@ -582,10 +582,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void setAttachmentClickListener(View view, StatusActionListener listener, int index, Attachment attachment, boolean animateTransition) { view.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { - listener.onContentHiddenChange(true, getAdapterPosition()); + listener.onContentHiddenChange(true, getBindingAdapterPosition()); } else { listener.onViewMedia(position, index, animateTransition ? v : null); } @@ -627,7 +627,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { displayName.setOnClickListener(profileButtonClickListener); replyButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onReply(position); } @@ -635,7 +635,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (reblogButton != null) { reblogButton.setEventListener((button, buttonState) -> { // return true to play animaion - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { if (statusDisplayOptions.confirmReblogs()) { showConfirmReblogDialog(listener, statusContent, buttonState, position); @@ -651,7 +651,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } favouriteButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onFavourite(!buttonState, position); } @@ -659,7 +659,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { }); bookmarkButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onBookmark(!buttonState, position); } @@ -667,7 +667,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { }); moreButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onMore(v, position); } @@ -677,7 +677,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { * just eat the clicks instead of deferring to the parent listener, but WILL respond to a * listener directly on the TextView, for whatever reason. */ View.OnClickListener viewThreadListener = v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onViewThread(position); } @@ -926,7 +926,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (expired || poll.getVoted()) { // no voting possible View.OnClickListener viewThreadListener = v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onViewThread(position); } @@ -958,7 +958,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 8755e8e80..abb8ca85a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -72,13 +72,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } reblogs.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onShowReblogs(position); } }); favourites.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onShowFavs(position); } 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 043b7b35e..68d64a698 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { hideStatusInfo(); } else { setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); - statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); + statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); } } @@ -105,7 +105,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { /* input filter for TextViews have to be set before text */ if (status.isCollapsible() && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) listener.onContentCollapsedChange(!status.isCollapsed(), position); }); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index e2236503d..bec07f067 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -98,7 +98,7 @@ class TabAdapter(private var data: List, } } binding.removeButton.setOnClickListener { - listener.onTabRemoved(holder.adapterPosition) + listener.onTabRemoved(holder.bindingAdapterPosition) } binding.removeButton.isEnabled = removeButtonEnabled ThemeUtils.setDrawableTint( @@ -131,7 +131,7 @@ class TabAdapter(private var data: List, } else { chip.setChipIconResource(R.drawable.ic_cancel_24dp) chip.setOnClickListener { - listener.onChipClicked(tab, holder.adapterPosition, i) + listener.onChipClicked(tab, holder.bindingAdapterPosition, i) } } } @@ -141,7 +141,7 @@ class TabAdapter(private var data: List, } binding.actionChip.setOnClickListener { - listener.onActionChipClicked(tab, holder.adapterPosition) + listener.onActionChipClicked(tab, holder.bindingAdapterPosition) } } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 093e860fb..a08aebc08 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -103,7 +103,7 @@ class MediaPreviewAdapter( progressImageView.layoutParams = layoutParams progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP progressImageView.setOnClickListener { - onMediaClick(adapterPosition, progressImageView) + onMediaClick(bindingAdapterPosition, progressImageView) } } } 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 e74be628c..2d2f683d0 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 @@ -147,7 +147,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) listener.onContentCollapsedChange(!collapsed, position); }); 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 index 7fd224aca..5ba3716eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -55,7 +55,7 @@ class DraftsAdapter( binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) binding.draftMediaPreview.adapter = DraftMediaAdapter { - getItem(viewHolder.adapterPosition)?.let { draft -> + getItem(viewHolder.bindingAdapterPosition)?.let { draft -> listener.onOpenDraft(draft) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt index de699d126..f475f3942 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -24,7 +24,7 @@ class DomainMutesAdapter( holder.binding.mutedDomain.text = instance holder.binding.mutedDomainUnmute.setOnClickListener { - actionListener.mute(false, instance, holder.adapterPosition) + actionListener.mute(false, instance, holder.bindingAdapterPosition) } } 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 88a246f40..90579a92c 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 @@ -173,5 +173,5 @@ class StatusViewHolder( } } - private fun status() = getStatusForPosition(adapterPosition) + private fun status() = getStatusForPosition(bindingAdapterPosition) } \ No newline at end of file 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 6e81ab352..cf7050b80 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -31,6 +32,7 @@ import com.keylesspalace.tusky.AccountListActivity.Type import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.* +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.databinding.FragmentAccountListBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account @@ -56,6 +58,8 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct @Inject lateinit var api: MastodonApi + @Inject + lateinit var accountManager: AccountManager private val binding by viewBinding(FragmentAccountListBinding::bind) @@ -90,10 +94,17 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct adapter = when (type) { Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis) Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis) - Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis) + Type.FOLLOW_REQUESTS -> { + val headerAdapter = FollowRequestsHeaderAdapter(accountManager.activeAccount!!.domain, arguments?.get(ARG_ACCOUNT_LOCKED) == true) + val followRequestsAdapter = FollowRequestsAdapter(this, animateAvatar, animateEmojis) + binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter) + followRequestsAdapter + } else -> FollowAdapter(this, animateAvatar, animateEmojis) } - binding.recyclerView.adapter = adapter + if (binding.recyclerView.adapter == null) { + binding.recyclerView.adapter = adapter + } scrollListener = object : EndlessOnScrollListener(layoutManager) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { @@ -361,12 +372,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct private const val TAG = "AccountList" // logging tag private const val ARG_TYPE = "type" private const val ARG_ID = "id" + private const val ARG_ACCOUNT_LOCKED = "acc_locked" - fun newInstance(type: Type, id: String? = null): AccountListFragment { + fun newInstance(type: Type, id: String? = null, accountLocked: Boolean = false): AccountListFragment { return AccountListFragment().apply { arguments = Bundle(2).apply { putSerializable(ARG_TYPE, type) putString(ARG_ID, id) + putBoolean(ARG_ACCOUNT_LOCKED, accountLocked) } } } 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 f66791920..c299e1153 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -320,7 +320,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr // saving some allocations override fun onClick(v: View?) { - viewMedia(items, adapterPosition, imageView) + viewMedia(items, bindingAdapterPosition, imageView) } } } diff --git a/app/src/main/res/layout/item_follow_requests_header.xml b/app/src/main/res/layout/item_follow_requests_header.xml new file mode 100644 index 000000000..06e5c93f3 --- /dev/null +++ b/app/src/main/res/layout/item_follow_requests_header.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ce2cb0dda..52f777b5d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -622,6 +622,9 @@ Draft deleted The Toot you drafted a reply to has been removed + Even though your account is not locked, the %1$s staff thought you might want to review follow requests from these accounts manually. + Subscribe Unsubscribe + From b195b42c4699f544f208400ca8d8973d2b812a20 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 11 Apr 2021 12:57:40 +0200 Subject: [PATCH 35/41] remove okhttp-tls dependency (#2132) --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index cf1137308..7253edb09 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -140,7 +140,6 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" - implementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" implementation "org.conscrypt:conscrypt-android:2.5.1" From 4d31542913b8e9cc07a4b55de93693f70999f0ae Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Wed, 14 Apr 2021 18:04:23 +0000 Subject: [PATCH 36/41] Translated using Weblate (Vietnamese) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (459 of 459 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 47 +++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d5aba7d5b..bb18628aa 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -68,13 +68,13 @@ Tải về Đang tải… Đã tải xong tập tin - Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và nhiều hơn nữa! + Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và vô số khác! \n \nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó. \n -\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng. +\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể dễ dàng giao tiếp và theo dõi mọi người trên các máy chủ khác. \n -\nTham khảo thêm tại joinmastodon.org. +\nTham khảo joinmastodon.org. Đang kết nối… Ảnh bìa Ảnh đại diện @@ -136,12 +136,12 @@ Tạo bình chọn Thêm tệp Mở trong trình duyệt - Album + Media Yêu cầu theo dõi Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Xem tút đã lưu + Lưu Thích Trang cá nhân Đóng @@ -186,7 +186,7 @@ Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Lưu + Những tút đã lưu Người theo dõi Theo dõi Ghim @@ -200,7 +200,7 @@ Thông báo Bảng tin Nháp - Thích + Những tút đã thích Máy chủ là gì\? Tải xem trước hình ảnh Hiện lượt trả lời @@ -237,7 +237,7 @@ Thông báo Thông báo Nhắn tin: Chỉ người được nhắc tới mới thấy - Người theo dõi: Ai đã theo dõi mới xem được + Người theo dõi: Ai đã theo dõi mới được xem Riêng tư: Không hiện trên bảng tin Công khai: Mọi người đều có thể thấy Ẩn @%s\? @@ -291,20 +291,20 @@ Cộng đồng xem thêm Trả lời @%s - Thư viện + Album 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 %ds - %d phút - %d giờ + %dm + %dh %d ngày %d năm %ds - %d phút - in %d giờ - in %d ngày - in %d năm + %dm + %dh + %dd + %dy Yêu cầu theo dõi Video Hình ảnh @@ -357,12 +357,12 @@ xong kết thúc lúc %s - %s người + %s người đã bình chọn - %s người + %s người bình chọn - %1$s • %2$s + %1$s • %2$s Mô tả cho hình %s Viết Viết tút @@ -383,7 +383,7 @@ Đã lưu Đã thích Đã chia sẻ - Không có mô tả + Âm thanh Nội dung nhạy cảm: %s tối đa %1$d tab @@ -402,14 +402,14 @@ Ghim Gỡ ghim Thông tin có thể hiển thị không đầy đủ. Nhấn để mở xem chi tiết trên trình duyệt. - Sử dụng thời gian của thiết bị + Sử dụng thời gian thiết bị Nội dung Nhãn thêm nội dung Metadata CC-BY-SA 4.0 CC-BY 4.0 - Licensed under the Apache License (sao chép bên dưới) + Giấy phép Apache (xem bên dưới) Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: Hủy chia sẻ Chia sẻ công khai @@ -462,11 +462,11 @@ Bỏ ẩn %s Ẩn tiêu đề tab Đã lưu! - Thêm ghi chú + Ghi chú Chưa có thông báo. Có gì mới\? Ẩn số liệu trên trang cá nhân - Ẩn tương tác trên tút + Ẩn số liệu 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: @@ -499,4 +499,5 @@ Emoji động Ngưng nhận thông báo Nhận thông báo + Dù biết tài khoản của bạn công khai, quản trị viên %1$s vẫn nghĩ bạn hãy nên xem lại yêu cầu theo dõi từ những tài khoản lạ. \ No newline at end of file From e55a06b84a1ea09b49ec7b60774520a01a7537d8 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Wed, 14 Apr 2021 18:04:24 +0000 Subject: [PATCH 37/41] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ --- app/src/main/res/values-no-rNB/strings.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index ca5a20f4e..f6c9f86ba 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -34,7 +34,7 @@ Favoritter Dempede brukere Blokkerte brukere - Forespørsler om følgen + Følgeforespørsler Endre profilen din Kladder Lisenser @@ -83,7 +83,7 @@ Favoritter Dempede brukere Blokkerte brukere - Forespørsler om følging + Følgeforespørsler Media Åpne i nettleser Legg til media @@ -452,8 +452,8 @@ %s personer Varsler om følgeforespørsler - Forespørsler om følging - følging forespurt + Følgeforespørsler + Følgeforespørsel sendt Dempe @%s\? Blokkere @%s\? Fjern demping av samtale @@ -510,4 +510,5 @@ Animer egendefinerte emojis Avslutt abonnementet Abonner + Selv om kontoen din ikke er låst, har %1$s administratorer markert disse følgeforespørsler for manuell godkjenning. \ No newline at end of file From 1ca27943db9fc112f09e6f74d99b51d538e45141 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 14 Apr 2021 18:04:24 +0000 Subject: [PATCH 38/41] Translated using Weblate (Galician) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ --- app/src/main/res/values-gl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index da0d2bc1d..bbba46cfe 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -510,4 +510,5 @@ Restablecer Programar Toot Teclado Emoji + Aínda que a túa conta non está bloqueada, a administración de %1$s opina que debes revisar manualmente as peticións de seguimento destas contas. \ No newline at end of file From ae93279ccbeee12d246876213601727ab05dff18 Mon Sep 17 00:00:00 2001 From: Connyduck Date: Wed, 14 Apr 2021 18:04:24 +0000 Subject: [PATCH 39/41] Translated using Weblate (German) Currently translated at 98.6% (454 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ --- app/src/main/res/values-de/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ccacb88b1..c5d26f031 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -500,4 +500,13 @@ Jemand, den ich abonniert habe, etwas Neues veröffentlicht %s hat gerade etwas gepostet %dm + Benachrichtigungen überprüfen + Informationen, die dein Wohlbefinden beeinflussen könnten, werden versteckt. Das beinhaltet +\n +\n- Benachrichtigungen über favorisierte/geteilte Beiträge, sowie \"Jemand folgt dir\" Benachrichtigungen +\n- Anzahl der Favoriten/Teilungen von Beiträgen +\n- Statistiken zu Followern auf Profilen +\n +\nPush-Benachrichtigungen sind nicht betroffen, aber du kannst diese manuell überprüfen. + Auch wenn dein Konto nicht gesperrt ist, haben die Admins von %1$s gedacht, dass es besser wäre diese Folgenden manuell zu bestätigen. \ No newline at end of file From 9b3308f538f6edec2360ff28a0202546c74249fc Mon Sep 17 00:00:00 2001 From: Deleted User Date: Wed, 14 Apr 2021 18:04:24 +0000 Subject: [PATCH 40/41] Translated using Weblate (German) Currently translated at 98.6% (454 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ --- app/src/main/res/values-de/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5d26f031..a67fafff0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -509,4 +509,9 @@ \n \nPush-Benachrichtigungen sind nicht betroffen, aber du kannst diese manuell überprüfen. Auch wenn dein Konto nicht gesperrt ist, haben die Admins von %1$s gedacht, dass es besser wäre diese Folgenden manuell zu bestätigen. + Keine Statistiken auf Profilen zeigen + Keine Statistiken in Posts zeigen + Timeline-Benachrichtigungen einschränken + Abonnieren + nicht mehr abonnieren \ No newline at end of file From bf6d7a6b975b8ad90f723555461c906e05f2ba56 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 22 Apr 2021 18:48:16 +0200 Subject: [PATCH 41/41] Convert TimelineFragment to Kotlin & ViewBinding (#2131) * convert TimelineFragment to Kotlin * cleanup some code * migrate to viewbinding * cleanup even more code * address review feedback * improve findStatusOrReblogPositionById --- .../tusky/fragment/SFragment.java | 4 +- .../tusky/fragment/TimelineFragment.java | 1526 ----------------- .../tusky/fragment/TimelineFragment.kt | 1265 ++++++++++++++ .../util/ListStatusAccessibilityDelegate.kt | 2 +- 4 files changed, 1268 insertions(+), 1529 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt 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 9d4b45b3e..ef1074a39 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -511,7 +511,7 @@ public abstract class SFragment extends Fragment implements Injectable { }); } - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public void reloadFilters(boolean forceRefresh) { if (filters != null && !forceRefresh) { applyFilters(forceRefresh); @@ -547,7 +547,7 @@ public abstract class SFragment extends Fragment implements Injectable { // Override to refresh your fragment } - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public boolean shouldFilterStatus(Status status) { if (filterRemoveRegex && status.getPoll() != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java deleted file mode 100644 index 6f42dd159..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ /dev/null @@ -1,1526 +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.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -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; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.AsyncDifferConfig; -import androidx.recyclerview.widget.AsyncListDiffer; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.ListUpdateCallback; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.keylesspalace.tusky.AccountListActivity; -import com.keylesspalace.tusky.BaseActivity; -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; -import com.keylesspalace.tusky.adapter.TimelineAdapter; -import com.keylesspalace.tusky.appstore.BlockEvent; -import com.keylesspalace.tusky.appstore.BookmarkEvent; -import com.keylesspalace.tusky.appstore.DomainMuteEvent; -import com.keylesspalace.tusky.appstore.EventHub; -import com.keylesspalace.tusky.appstore.FavoriteEvent; -import com.keylesspalace.tusky.appstore.MuteConversationEvent; -import com.keylesspalace.tusky.appstore.MuteEvent; -import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; -import com.keylesspalace.tusky.appstore.ReblogEvent; -import com.keylesspalace.tusky.appstore.StatusComposedEvent; -import com.keylesspalace.tusky.appstore.StatusDeletedEvent; -import com.keylesspalace.tusky.appstore.UnfollowEvent; -import com.keylesspalace.tusky.db.AccountManager; -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.interfaces.ActionButtonActivity; -import com.keylesspalace.tusky.interfaces.RefreshableFragment; -import com.keylesspalace.tusky.interfaces.ReselectableFragment; -import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.repository.Placeholder; -import com.keylesspalace.tusky.repository.TimelineRepository; -import com.keylesspalace.tusky.repository.TimelineRequestMode; -import com.keylesspalace.tusky.settings.PrefKeys; -import com.keylesspalace.tusky.util.CardViewMode; -import com.keylesspalace.tusky.util.Either; -import com.keylesspalace.tusky.util.HttpHeaderLink; -import com.keylesspalace.tusky.util.LinkHelper; -import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; -import com.keylesspalace.tusky.util.ListUtils; -import com.keylesspalace.tusky.util.PairedList; -import com.keylesspalace.tusky.util.StatusDisplayOptions; -import com.keylesspalace.tusky.util.StringUtils; -import com.keylesspalace.tusky.util.ViewDataUtils; -import com.keylesspalace.tusky.view.BackgroundMessageView; -import com.keylesspalace.tusky.view.EndlessOnScrollListener; -import com.keylesspalace.tusky.viewdata.StatusViewData; - -import java.io.IOException; -import java.util.ArrayList; -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; -import kotlin.jvm.functions.Function1; -import retrofit2.Response; - -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; - -public class TimelineFragment extends SFragment implements - SwipeRefreshLayout.OnRefreshListener, - StatusActionListener, - Injectable, ReselectableFragment, RefreshableFragment { - private static final String TAG = "TimelineF"; // logging tag - private static final String KIND_ARG = "kind"; - private static final String ID_ARG = "id"; - private static final String HASHTAGS_ARG = "hastags"; - private static final String ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"; - - private static final int LOAD_AT_ONCE = 30; - private boolean isSwipeToRefreshEnabled = true; - private boolean isNeedRefresh; - - public enum Kind { - HOME, - PUBLIC_LOCAL, - PUBLIC_FEDERATED, - TAG, - USER, - USER_PINNED, - USER_WITH_REPLIES, - FAVOURITES, - LIST, - BOOKMARKS - } - - private enum FetchEnd { - TOP, - BOTTOM, - MIDDLE - } - - @Inject - public EventHub eventHub; - @Inject - TimelineRepository timelineRepo; - - @Inject - public AccountManager accountManager; - - private boolean eventRegistered = false; - - private SwipeRefreshLayout swipeRefreshLayout; - private RecyclerView recyclerView; - private ProgressBar progressBar; - private ContentLoadingProgressBar topProgressBar; - private BackgroundMessageView statusView; - - private TimelineAdapter adapter; - private Kind kind; - private String id; - private List tags; - /** - * For some timeline kinds we must use LINK headers and not just status ids. - */ - private String nextId; - private LinearLayoutManager layoutManager; - private EndlessOnScrollListener scrollListener; - private boolean filterRemoveReplies; - private boolean filterRemoveReblogs; - private boolean hideFab; - private boolean bottomLoading; - - private boolean didLoadEverythingBottom; - private boolean alwaysShowSensitiveMedia; - private boolean alwaysOpenSpoiler; - private boolean initialUpdateFailed = false; - - private PairedList, StatusViewData> statuses = - new PairedList<>(new Function, StatusViewData>() { - @Override - public StatusViewData apply(Either input) { - Status status = input.asRightOrNull(); - if (status != null) { - return ViewDataUtils.statusToViewData( - status, - alwaysShowSensitiveMedia, - alwaysOpenSpoiler - ); - } else { - Placeholder placeholder = input.asLeft(); - return new StatusViewData.Placeholder(placeholder.getId(), false); - } - } - }); - - public static TimelineFragment newInstance(Kind kind) { - return newInstance(kind, null); - } - - public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId) { - return newInstance(kind, hashtagOrId, true); - } - - public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh) { - TimelineFragment fragment = new TimelineFragment(); - Bundle arguments = new Bundle(3); - arguments.putString(KIND_ARG, kind.name()); - arguments.putString(ID_ARG, hashtagOrId); - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh); - fragment.setArguments(arguments); - return fragment; - } - - public static TimelineFragment newHashtagInstance(@NonNull List hashtags) { - TimelineFragment fragment = new TimelineFragment(); - Bundle arguments = new Bundle(3); - arguments.putString(KIND_ARG, Kind.TAG.name()); - arguments.putStringArrayList(HASHTAGS_ARG, new ArrayList<>(hashtags)); - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle arguments = requireArguments(); - kind = Kind.valueOf(arguments.getString(KIND_ARG)); - if (kind == Kind.USER - || kind == Kind.USER_PINNED - || kind == Kind.USER_WITH_REPLIES - || kind == Kind.LIST) { - id = arguments.getString(ID_ARG); - } - if (kind == Kind.TAG) { - tags = arguments.getStringArrayList(HASHTAGS_ARG); - } - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( - preferences.getBoolean("animateGifAvatars", false), - accountManager.getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), - preferences.getBoolean("showCardsInTimelines", false) ? - CardViewMode.INDENTED : - CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - ); - adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); - - isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); - - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false); - - recyclerView = rootView.findViewById(R.id.recyclerView); - swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); - progressBar = rootView.findViewById(R.id.progressBar); - statusView = rootView.findViewById(R.id.statusView); - topProgressBar = rootView.findViewById(R.id.topProgressBar); - - setupSwipeRefreshLayout(); - setupRecyclerView(); - updateAdapter(); - setupTimelinePreferences(); - - if (statuses.isEmpty()) { - progressBar.setVisibility(View.VISIBLE); - bottomLoading = true; - this.sendInitialRequest(); - } else { - progressBar.setVisibility(View.GONE); - if (isNeedRefresh) - onRefresh(); - } - - return rootView; - } - - private void sendInitialRequest() { - if (this.kind == Kind.HOME) { - this.tryCache(); - } else { - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - } - - private void tryCache() { - // Request timeline from disk to make it quick, then replace it with timeline from - // the server to update it - this.timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, - TimelineRequestMode.DISK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(statuses -> { - filterStatuses(statuses); - - if (statuses.size() > 1) { - this.clearPlaceholdersForResponse(statuses); - this.statuses.clear(); - this.statuses.addAll(statuses); - this.updateAdapter(); - this.progressBar.setVisibility(View.GONE); - // Request statuses including current top to refresh all of them - } - - this.updateCurrent(); - this.loadAbove(); - }); - } - - private void updateCurrent() { - if (this.statuses.isEmpty()) { - return; - } - - String topId = CollectionsKt.first(this.statuses, Either::isRight).asRight().getId(); - - this.timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, - TimelineRequestMode.NETWORK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (statuses) -> { - this.initialUpdateFailed = false; - // When cached timeline is too old, we would replace it with nothing - if (!statuses.isEmpty()) { - filterStatuses(statuses); - - if (!this.statuses.isEmpty()) { - // clear old cached statuses - Iterator> iterator = this.statuses.iterator(); - while (iterator.hasNext()) { - Either item = iterator.next(); - if (item.isRight()) { - Status status = item.asRight(); - if (status.getId().length() < topId.length() || status.getId().compareTo(topId) < 0) { - - iterator.remove(); - } - } else { - Placeholder placeholder = item.asLeft(); - if (placeholder.getId().length() < topId.length() || placeholder.getId().compareTo(topId) < 0) { - - iterator.remove(); - } - } - - } - } - - this.statuses.addAll(statuses); - this.updateAdapter(); - } - this.bottomLoading = false; - - }, - (e) -> { - this.initialUpdateFailed = true; - // Indicate that we are not loading anymore - this.progressBar.setVisibility(View.GONE); - this.swipeRefreshLayout.setRefreshing(false); - }); - } - - private void setupTimelinePreferences() { - alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); - alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean filter = preferences.getBoolean("tabFilterHomeReplies", true); - filterRemoveReplies = kind == Kind.HOME && !filter; - - filter = preferences.getBoolean("tabFilterHomeBoosts", true); - filterRemoveReblogs = kind == Kind.HOME && !filter; - reloadFilters(false); - } - - private static boolean filterContextMatchesKind(Kind kind, List filterContext) { - // home, notifications, public, thread - switch (kind) { - case HOME: - case LIST: - return filterContext.contains(Filter.HOME); - case PUBLIC_FEDERATED: - case PUBLIC_LOCAL: - case TAG: - return filterContext.contains(Filter.PUBLIC); - case FAVOURITES: - return (filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS)); - case USER: - case USER_WITH_REPLIES: - case USER_PINNED: - return filterContext.contains(Filter.ACCOUNT); - default: - return false; - } - } - - @Override - protected boolean filterIsRelevant(@NonNull Filter filter) { - return filterContextMatchesKind(kind, filter.getContext()); - } - - @Override - protected void refreshAfterApplyingFilters() { - fullyRefresh(); - } - - private void setupSwipeRefreshLayout() { - swipeRefreshLayout.setEnabled(isSwipeToRefreshEnabled); - if (isSwipeToRefreshEnabled) { - swipeRefreshLayout.setOnRefreshListener(this); - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); - } - } - - private void setupRecyclerView() { - recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); - Context context = recyclerView.getContext(); - recyclerView.setHasFixedSize(true); - layoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(layoutManager); - DividerItemDecoration divider = new DividerItemDecoration( - context, layoutManager.getOrientation()); - recyclerView.addItemDecoration(divider); - - // CWs are expanded without animation, buttons animate itself, we don't need it basically - ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); - - recyclerView.setAdapter(adapter); - } - - private void deleteStatusById(String id) { - for (int i = 0; i < statuses.size(); i++) { - Either either = statuses.get(i); - if (either.isRight() - && id.equals(either.asRight().getId())) { - statuses.remove(either); - updateAdapter(); - break; - } - } - if (statuses.size() == 0) { - showNothing(); - } - } - - private void showNothing() { - statusView.setVisibility(View.VISIBLE); - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't - * guaranteed to be set until then. */ - if (actionButtonPresent()) { - /* Use a modified scroll listener that both loads more statuses as it goes, and hides - * the follow button on down-scroll. */ - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - hideFab = preferences.getBoolean("fabHide", false); - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - super.onScrolled(view, dx, dy); - - ActionButtonActivity activity = (ActionButtonActivity) getActivity(); - FloatingActionButton composeButton = activity.getActionButton(); - - if (composeButton != null) { - if (hideFab) { - if (dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down - } else if (dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up - } - } else if (!composeButton.isShown()) { - composeButton.show(); - } - } - } - - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; - } else { - // Just use the basic scroll listener to load more statuses. - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; - } - recyclerView.addOnScrollListener(scrollListener); - - if (!eventRegistered) { - eventHub.getEvents() - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(event -> { - if (event instanceof FavoriteEvent) { - FavoriteEvent favEvent = ((FavoriteEvent) event); - handleFavEvent(favEvent); - } else if (event instanceof ReblogEvent) { - ReblogEvent reblogEvent = (ReblogEvent) event; - handleReblogEvent(reblogEvent); - } else if (event instanceof BookmarkEvent) { - BookmarkEvent bookmarkEvent = (BookmarkEvent) event; - handleBookmarkEvent(bookmarkEvent); - } else if (event instanceof MuteConversationEvent) { - MuteConversationEvent muteEvent = (MuteConversationEvent) event; - handleMuteConversationEvent(muteEvent); - } else if (event instanceof UnfollowEvent) { - if (kind == Kind.HOME) { - String id = ((UnfollowEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof BlockEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((BlockEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof MuteEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((MuteEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof DomainMuteEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String instance = ((DomainMuteEvent) event).getInstance(); - removeAllByInstance(instance); - } - } else if (event instanceof StatusDeletedEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((StatusDeletedEvent) event).getStatusId(); - deleteStatusById(id); - } - } else if (event instanceof StatusComposedEvent) { - Status status = ((StatusComposedEvent) event).getStatus(); - handleStatusComposeEvent(status); - } else if (event instanceof PreferenceChangedEvent) { - onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey()); - } - }); - eventRegistered = true; - } - } - - @Override - public void onRefresh() { - if (isSwipeToRefreshEnabled) - swipeRefreshLayout.setEnabled(true); - this.statusView.setVisibility(View.GONE); - isNeedRefresh = false; - if (this.initialUpdateFailed) { - updateCurrent(); - } - - this.loadAbove(); - - } - - private void loadAbove() { - String firstOrNull = null; - String secondOrNull = null; - for (int i = 0; i < this.statuses.size(); i++) { - Either status = this.statuses.get(i); - if (status.isRight()) { - firstOrNull = status.asRight().getId(); - if (i + 1 < statuses.size() && statuses.get(i + 1).isRight()) { - secondOrNull = statuses.get(i + 1).asRight().getId(); - } - break; - } - } - if (firstOrNull != null) { - this.sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1); - } else { - this.sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - } - - @Override - public void onReply(int position) { - super.reply(statuses.get(position).asRight()); - } - - @Override - public void onReblog(final boolean reblog, final int position) { - final Status status = statuses.get(position).asRight(); - timelineCases.reblog(status, reblog) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setRebloggedForStatus(position, status, reblog), - (err) -> Log.d(TAG, "Failed to reblog status " + status.getId(), err) - ); - } - - private void setRebloggedForStatus(int position, Status status, boolean reblog) { - status.setReblogged(reblog); - - if (status.getReblog() != null) { - status.getReblog().setReblogged(reblog); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = - new StatusViewData.Builder(actual.first) - .setReblogged(reblog) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onFavourite(final boolean favourite, final int position) { - final Status status = statuses.get(position).asRight(); - - timelineCases.favourite(status, favourite) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setFavouriteForStatus(position, newStatus, favourite), - (err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) - ); - } - - private void setFavouriteForStatus(int position, Status status, boolean favourite) { - status.setFavourited(favourite); - - if (status.getReblog() != null) { - status.getReblog().setFavourited(favourite); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setFavourited(favourite) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onBookmark(final boolean bookmark, final int position) { - final Status status = statuses.get(position).asRight(); - - timelineCases.bookmark(status, bookmark) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), - (err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) - ); - } - - private void setBookmarkForStatus(int position, Status status, boolean bookmark) { - status.setBookmarked(bookmark); - - if (status.getReblog() != null) { - status.getReblog().setBookmarked(bookmark); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setBookmarked(bookmark) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - public void onVoteInPoll(int position, @NonNull List choices) { - - final Status status = statuses.get(position).asRight(); - - Poll votedPoll = status.getActionableStatus().getPoll().votedCopy(choices); - - setVoteForPoll(position, status, votedPoll); - - timelineCases.voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) - .subscribe( - (newPoll) -> setVoteForPoll(position, status, newPoll), - (t) -> Log.d(TAG, - "Failed to vote in poll: " + status.getId(), t) - ); - } - - private void setVoteForPoll(int position, Status status, Poll newPoll) { - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setPoll(newPoll) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onMore(@NonNull View view, final int position) { - super.more(statuses.get(position).asRight(), view, position); - } - - @Override - public void onOpenReblog(int position) { - super.openReblog(statuses.get(position).asRight()); - } - - @Override - public void onExpandedChange(boolean expanded, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem(position))) - .setIsExpanded(expanded).createStatusViewData(); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } - - @Override - public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem(position))) - .setIsShowingSensitiveContent(isShowing).createStatusViewData(); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } - - - @Override - public void onShowReblogs(int position) { - String statusId = statuses.get(position).asRight().getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onShowFavs(int position) { - String statusId = statuses.get(position).asRight().getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onLoadMore(int position) { - //check bounds before accessing list, - if (statuses.size() >= position && position > 0) { - Status fromStatus = statuses.get(position - 1).asRightOrNull(); - Status toStatus = statuses.get(position + 1).asRightOrNull(); - String maxMinusOne = - statuses.size() > position + 1 && statuses.get(position + 2).isRight() - ? statuses.get(position + 1).asRight().getId() - : null; - if (fromStatus == null || toStatus == null) { - Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position"); - return; - } - sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne, - FetchEnd.MIDDLE, position); - - Placeholder placeholder = statuses.get(position).asLeft(); - StatusViewData newViewData = new StatusViewData.Placeholder(placeholder.getId(), true); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } else { - Log.e(TAG, "error loading more"); - } - } - - @Override - public void onContentCollapsedChange(boolean isCollapsed, int position) { - if (position < 0 || position >= statuses.size()) { - Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size() - 1)); - return; - } - - StatusViewData status = statuses.getPairedItem(position); - if (!(status instanceof StatusViewData.Concrete)) { - // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Log.e(TAG, String.format( - "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", - status == null ? "" : status.getClass().getSimpleName(), - position, - statuses.size() - 1 - )); - return; - } - - StatusViewData updatedStatus = new StatusViewData.Builder((StatusViewData.Concrete) status) - .setCollapsed(isCollapsed) - .createStatusViewData(); - statuses.setPairedItem(position, updatedStatus); - updateAdapter(); - } - - @Override - public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { - Status status = statuses.get(position).asRightOrNull(); - if (status == null) return; - super.viewMedia(attachmentIndex, status, view); - } - - @Override - public void onViewThread(int position) { - super.viewThread(statuses.get(position).asRight()); - } - - @Override - public void onViewTag(String tag) { - if (kind == Kind.TAG && tags.size() == 1 && tags.contains(tag)) { - // If already viewing a tag page, then ignore any request to view that tag again. - return; - } - super.viewTag(tag); - } - - @Override - public void onViewAccount(String id) { - if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id.equals(id)) { - /* If already viewing an account page, then any requests to view that account page - * should be ignored. */ - return; - } - super.viewAccount(id); - } - - private void onPreferenceChanged(String key) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - switch (key) { - case "fabHide": { - hideFab = sharedPreferences.getBoolean("fabHide", false); - break; - } - case "mediaPreviewEnabled": { - boolean enabled = accountManager.getActiveAccount().getMediaPreviewEnabled(); - boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled(); - if (enabled != oldMediaPreviewEnabled) { - adapter.setMediaPreviewEnabled(enabled); - fullyRefresh(); - } - break; - } - case "tabFilterHomeReplies": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true); - boolean oldRemoveReplies = filterRemoveReplies; - filterRemoveReplies = kind == Kind.HOME && !filter; - if (adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) { - fullyRefresh(); - } - break; - } - case "tabFilterHomeBoosts": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true); - boolean oldRemoveReblogs = filterRemoveReblogs; - filterRemoveReblogs = kind == Kind.HOME && !filter; - if (adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) { - fullyRefresh(); - } - break; - } - case Filter.HOME: - case Filter.NOTIFICATIONS: - case Filter.THREAD: - case Filter.PUBLIC: - case Filter.ACCOUNT: { - if (filterContextMatchesKind(kind, Collections.singletonList(key))) { - reloadFilters(true); - } - break; - } - case "alwaysShowSensitiveMedia": { - //it is ok if only newly loaded statuses are affected, no need to fully refresh - alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); - break; - } - } - } - - @Override - public void removeItem(int position) { - statuses.remove(position); - updateAdapter(); - } - - private void removeAllByAccountId(String accountId) { - // using iterator to safely remove items while iterating - Iterator> iterator = statuses.iterator(); - while (iterator.hasNext()) { - Status status = iterator.next().asRightOrNull(); - if (status != null && - (status.getAccount().getId().equals(accountId) || status.getActionableStatus().getAccount().getId().equals(accountId))) { - iterator.remove(); - } - } - updateAdapter(); - } - - private void removeAllByInstance(String instance) { - // using iterator to safely remove items while iterating - Iterator> iterator = statuses.iterator(); - while (iterator.hasNext()) { - Status status = iterator.next().asRightOrNull(); - if (status != null && LinkHelper.getDomain(status.getAccount().getUrl()).equals(instance)) { - iterator.remove(); - } - } - updateAdapter(); - } - - private void onLoadMore() { - if (didLoadEverythingBottom || bottomLoading) { - return; - } - - if (statuses.size() == 0) { - sendInitialRequest(); - return; - } - - bottomLoading = true; - - Either last = statuses.get(statuses.size() - 1); - Placeholder placeholder; - if (last.isRight()) { - final String placeholderId = StringUtils.dec(last.asRight().getId()); - placeholder = new Placeholder(placeholderId); - statuses.add(new Either.Left<>(placeholder)); - } else { - placeholder = last.asLeft(); - } - statuses.setPairedItem(statuses.size() - 1, - new StatusViewData.Placeholder(placeholder.getId(), true)); - - updateAdapter(); - - String bottomId = null; - if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { - bottomId = this.nextId; - } else { - final ListIterator> iterator = - this.statuses.listIterator(this.statuses.size()); - while (iterator.hasPrevious()) { - Either previous = iterator.previous(); - if (previous.isRight()) { - bottomId = previous.asRight().getId(); - break; - } - } - } - sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1); - } - - private void fullyRefresh() { - statuses.clear(); - updateAdapter(); - bottomLoading = true; - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - - private boolean actionButtonPresent() { - return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && - getActivity() instanceof ActionButtonActivity; - } - - private void jumpToTop() { - if (isAdded()) { - layoutManager.scrollToPosition(0); - recyclerView.stopScroll(); - scrollListener.reset(); - } - } - - private Single>> getFetchCallByTimelineType(String fromId, String uptoId) { - MastodonApi api = mastodonApi; - switch (kind) { - default: - case HOME: - return api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE); - case PUBLIC_FEDERATED: - return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE); - case PUBLIC_LOCAL: - return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE); - case TAG: - String firstHashtag = tags.get(0); - List additionalHashtags = tags.subList(1, tags.size()); - return api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE); - case USER: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null); - case USER_PINNED: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true); - case USER_WITH_REPLIES: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null); - case FAVOURITES: - return api.favourites(fromId, uptoId, LOAD_AT_ONCE); - case BOOKMARKS: - return api.bookmarks(fromId, uptoId, LOAD_AT_ONCE); - case LIST: - return api.listTimeline(id, fromId, uptoId, LOAD_AT_ONCE); - } - } - - private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId, - @Nullable String sinceIdMinusOne, - final FetchEnd fetchEnd, final int pos) { - if (isAdded() && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && progressBar.getVisibility() != View.VISIBLE) && !isSwipeToRefreshEnabled) - topProgressBar.show(); - - if (kind == Kind.HOME) { - TimelineRequestMode mode; - // allow getting old statuses/fallbacks for network only for for bottom loading - if (fetchEnd == FetchEnd.BOTTOM) { - mode = TimelineRequestMode.ANY; - } else { - mode = TimelineRequestMode.NETWORK; - } - timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - result -> onFetchTimelineSuccess(result, fetchEnd, pos), - err -> onFetchTimelineFailure(err, fetchEnd, pos) - ); - } else { - 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) - ); - } - } - - @Nullable - private String extractNextId(Response response) { - String linkHeader = response.headers().get("Link"); - if (linkHeader == null) { - return null; - } - List links = HttpHeaderLink.parse(linkHeader); - HttpHeaderLink nextHeader = HttpHeaderLink.findByRelationType(links, "next"); - if (nextHeader == null) { - return null; - } - Uri nextLink = nextHeader.uri; - if (nextLink == null) { - return null; - } - return nextLink.getQueryParameter("max_id"); - } - - private void onFetchTimelineSuccess(List> statuses, - FetchEnd fetchEnd, int pos) { - - // We filled the hole (or reached the end) if the server returned less statuses than we - // we asked for. - boolean fullFetch = statuses.size() >= LOAD_AT_ONCE; - filterStatuses(statuses); - switch (fetchEnd) { - case TOP: { - updateStatuses(statuses, fullFetch); - break; - } - case MIDDLE: { - replacePlaceholderWithStatuses(statuses, fullFetch, pos); - break; - } - case BOTTOM: { - if (!this.statuses.isEmpty() - && !this.statuses.get(this.statuses.size() - 1).isRight()) { - this.statuses.remove(this.statuses.size() - 1); - updateAdapter(); - } - - if (!statuses.isEmpty() && !statuses.get(statuses.size() - 1).isRight()) { - // Removing placeholder if it's the last one from the cache - statuses.remove(statuses.size() - 1); - } - int oldSize = this.statuses.size(); - if (this.statuses.size() > 1) { - addItems(statuses); - } else { - updateStatuses(statuses, fullFetch); - } - if (this.statuses.size() == oldSize) { - // This may be a brittle check but seems like it works - // Can we check it using headers somehow? Do all server support them? - didLoadEverythingBottom = true; - } - break; - } - } - if (isAdded()) { - topProgressBar.hide(); - updateBottomLoadingState(fetchEnd); - progressBar.setVisibility(View.GONE); - swipeRefreshLayout.setRefreshing(false); - swipeRefreshLayout.setEnabled(true); - if (this.statuses.size() == 0) { - this.showNothing(); - } else { - this.statusView.setVisibility(View.GONE); - } - } - } - - private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) { - if (isAdded()) { - swipeRefreshLayout.setRefreshing(false); - topProgressBar.hide(); - - if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { - Placeholder placeholder = statuses.get(position).asLeftOrNull(); - StatusViewData newViewData; - if (placeholder == null) { - Status above = statuses.get(position - 1).asRight(); - String newId = StringUtils.dec(above.getId()); - placeholder = new Placeholder(newId); - } - newViewData = new StatusViewData.Placeholder(placeholder.getId(), false); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } else if (this.statuses.isEmpty()) { - swipeRefreshLayout.setEnabled(false); - this.statusView.setVisibility(View.VISIBLE); - if (throwable instanceof IOException) { - this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); - } else { - this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); - } - } - - Log.e(TAG, "Fetch Failure: " + throwable.getMessage()); - updateBottomLoadingState(fetchEnd); - progressBar.setVisibility(View.GONE); - } - } - - private void updateBottomLoadingState(FetchEnd fetchEnd) { - if (fetchEnd == FetchEnd.BOTTOM) { - bottomLoading = false; - } - } - - private void filterStatuses(List> statuses) { - Iterator> it = statuses.iterator(); - while (it.hasNext()) { - Status status = it.next().asRightOrNull(); - if (status != null - && ((status.getInReplyToId() != null && filterRemoveReplies) - || (status.getReblog() != null && filterRemoveReblogs) - || shouldFilterStatus(status.getActionableStatus()))) { - it.remove(); - } - } - } - - private void updateStatuses(List> newStatuses, boolean fullFetch) { - if (ListUtils.isEmpty(newStatuses)) { - updateAdapter(); - return; - } - - if (statuses.isEmpty()) { - statuses.addAll(newStatuses); - } else { - Either lastOfNew = newStatuses.get(newStatuses.size() - 1); - int index = statuses.indexOf(lastOfNew); - - if (index >= 0) { - statuses.subList(0, index).clear(); - } - - int newIndex = newStatuses.indexOf(statuses.get(0)); - if (newIndex == -1) { - if (index == -1 && fullFetch) { - String placeholderId = StringUtils.inc( - CollectionsKt.last(newStatuses, Either::isRight).asRight().getId()); - newStatuses.add(new Either.Left<>(new Placeholder(placeholderId))); - } - statuses.addAll(0, newStatuses); - } else { - statuses.addAll(0, newStatuses.subList(0, newIndex)); - } - } - // Remove all consecutive placeholders - removeConsecutivePlaceholders(); - updateAdapter(); - } - - private void removeConsecutivePlaceholders() { - for (int i = 0; i < statuses.size() - 1; i++) { - if (statuses.get(i).isLeft() && statuses.get(i + 1).isLeft()) { - statuses.remove(i); - } - } - } - - private void addItems(List> newStatuses) { - if (ListUtils.isEmpty(newStatuses)) { - return; - } - Either last = null; - for (int i = statuses.size() - 1; i >= 0; i--) { - if (statuses.get(i).isRight()) { - last = statuses.get(i); - break; - } - } - // I was about to replace findStatus with indexOf but it is incorrect to compare value - // types by ID anyway and we should change equals() for Status, I think, so this makes sense - if (last != null && !newStatuses.contains(last)) { - statuses.addAll(newStatuses); - removeConsecutivePlaceholders(); - updateAdapter(); - } - } - - /** - * For certain requests we don't want to see placeholders, they will be removed some other way - */ - private void clearPlaceholdersForResponse(List> statuses) { - CollectionsKt.removeAll(statuses, Either::isLeft); - } - - private void replacePlaceholderWithStatuses(List> newStatuses, - boolean fullFetch, int pos) { - Either placeholder = statuses.get(pos); - if (placeholder.isLeft()) { - statuses.remove(pos); - } - - if (ListUtils.isEmpty(newStatuses)) { - updateAdapter(); - return; - } - - if (fullFetch) { - newStatuses.add(placeholder); - } - - statuses.addAll(pos, newStatuses); - removeConsecutivePlaceholders(); - - updateAdapter(); - - } - - private int findStatusOrReblogPositionById(@NonNull String statusId) { - for (int i = 0; i < statuses.size(); i++) { - Status status = statuses.get(i).asRightOrNull(); - if (status != null - && (statusId.equals(status.getId()) - || (status.getReblog() != null - && statusId.equals(status.getReblog().getId())))) { - return i; - } - } - return -1; - } - - private final Function1> statusLifter = - Either.Right::new; - - @Nullable - private Pair - findStatusAndPosition(int position, Status status) { - StatusViewData.Concrete statusToUpdate; - int positionToUpdate; - StatusViewData someOldViewData = statuses.getPairedItem(position); - - // Unlikely, but data could change between the request and response - if ((someOldViewData instanceof StatusViewData.Placeholder) || - !((StatusViewData.Concrete) someOldViewData).getId().equals(status.getId())) { - // try to find the status we need to update - int foundPos = statuses.indexOf(new Either.Right<>(status)); - if (foundPos < 0) return null; // okay, it's hopeless, give up - statusToUpdate = ((StatusViewData.Concrete) - statuses.getPairedItem(foundPos)); - positionToUpdate = position; - } else { - statusToUpdate = (StatusViewData.Concrete) someOldViewData; - positionToUpdate = position; - } - return new Pair<>(statusToUpdate, positionToUpdate); - } - - private void handleReblogEvent(@NonNull ReblogEvent reblogEvent) { - int pos = findStatusOrReblogPositionById(reblogEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setRebloggedForStatus(pos, status, reblogEvent.getReblog()); - } - - private void handleFavEvent(@NonNull FavoriteEvent favEvent) { - int pos = findStatusOrReblogPositionById(favEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setFavouriteForStatus(pos, status, favEvent.getFavourite()); - } - - private void handleBookmarkEvent(@NonNull BookmarkEvent bookmarkEvent) { - int pos = findStatusOrReblogPositionById(bookmarkEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark()); - } - - private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) { - fullyRefresh(); - } - - private void handleStatusComposeEvent(@NonNull Status status) { - switch (kind) { - case HOME: - case PUBLIC_FEDERATED: - case PUBLIC_LOCAL: - break; - case USER: - case USER_WITH_REPLIES: - if (status.getAccount().getId().equals(id)) { - break; - } else { - return; - } - case TAG: - case FAVOURITES: - case LIST: - return; - } - onRefresh(); - } - - private List> liftStatusList(List list) { - return CollectionsKt.map(list, statusLifter); - } - - private void updateAdapter() { - differ.submitList(statuses.getPairedCopy()); - } - - private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - if (isAdded()) { - adapter.notifyItemRangeInserted(position, count); - Context context = getContext(); - // scroll up when new items at the top are loaded while being in the first position - // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 - if (position == 0 && context != null && adapter.getItemCount() != count) { - if (isSwipeToRefreshEnabled) - recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); - else - recyclerView.scrollToPosition(0); - } - } - } - - @Override - public void onRemoved(int position, int count) { - adapter.notifyItemRangeRemoved(position, count); - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - adapter.notifyItemMoved(fromPosition, toPosition); - } - - @Override - public void onChanged(int position, int count, Object payload) { - adapter.notifyItemRangeChanged(position, count, payload); - } - }; - - - private final AsyncListDiffer - differ = new AsyncListDiffer<>(listUpdateCallback, - new AsyncDifferConfig.Builder<>(diffCallback).build()); - - private final TimelineAdapter.AdapterDataSource dataSource = - new TimelineAdapter.AdapterDataSource() { - @Override - public int getItemCount() { - return differ.getCurrentList().size(); - } - - @Override - public StatusViewData getItemAt(int pos) { - return differ.getCurrentList().get(pos); - } - }; - - private static final DiffUtil.ItemCallback diffCallback - = new DiffUtil.ItemCallback() { - - @Override - public boolean areItemsTheSame(StatusViewData oldItem, StatusViewData newItem) { - return oldItem.getViewDataId() == newItem.getViewDataId(); - } - - @Override - public boolean areContentsTheSame(StatusViewData oldItem, @NonNull StatusViewData newItem) { - return false; //Items are different always. It allows to refresh timestamp on every view holder update - } - - @Nullable - @Override - public Object getChangePayload(@NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) { - if (oldItem.deepEquals(newItem)) { - //If items are equal - update timestamp only - return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); - } else - // If items are different - update a whole view holder - return null; - } - }; - - 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(); - } - - /** - * Start to update adapter every minute to refresh timestamp - * If setting absoluteTimeView is false - * Auto dispose observable on pause - */ - private void startUpdateTimestamp() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); - if (!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) - .subscribe( - interval -> updateAdapter() - ); - } - - } - - @Override - public void onReselect() { - jumpToTop(); - } - - @Override - public void refreshContent() { - if (isAdded()) - onRefresh(); - else - isNeedRefresh = true; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt new file mode 100644 index 000000000..bb42e46fc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt @@ -0,0 +1,1265 @@ +/* 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.fragment + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityManager +import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.sparkbutton.helpers.Utils +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.TimelineAdapter +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.MuteConversationEvent +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.db.AccountManager +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.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.repository.Placeholder +import com.keylesspalace.tusky.repository.TimelineRepository +import com.keylesspalace.tusky.repository.TimelineRequestMode +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.Either.Left +import com.keylesspalace.tusky.util.Either.Right +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.PairedList +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.ViewDataUtils +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import retrofit2.Response +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, ReselectableFragment, RefreshableFragment { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var timelineRepo: TimelineRepository + + @Inject + lateinit var accountManager: AccountManager + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private var kind: Kind? = null + private var id: String? = null + private var tags: List = emptyList() + + private lateinit var adapter: TimelineAdapter + + private var isSwipeToRefreshEnabled = true + private var isNeedRefresh = false + + private var eventRegistered = false + + /** + * For some timeline kinds we must use LINK headers and not just status ids. + */ + private var nextId: String? = null + private var layoutManager: LinearLayoutManager? = null + private var scrollListener: EndlessOnScrollListener? = null + private var filterRemoveReplies = false + private var filterRemoveReblogs = false + private var hideFab = false + private var bottomLoading = false + private var didLoadEverythingBottom = false + private var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoiler = false + private var initialUpdateFailed = false + + private val statuses = PairedList, StatusViewData> { input -> + val status = input.asRightOrNull() + if (status != null) { + ViewDataUtils.statusToViewData( + status, + alwaysShowSensitiveMedia, + alwaysOpenSpoiler + ) + } else { + val (id1) = input.asLeft() + StatusViewData.Placeholder(id1, false) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val arguments = requireArguments() + kind = Kind.valueOf(arguments.getString(KIND_ARG)!!) + if (kind == Kind.USER || kind == Kind.USER_PINNED || kind == Kind.USER_WITH_REPLIES || kind == Kind.LIST) { + id = arguments.getString(ID_ARG)!! + } + if (kind == Kind.TAG) { + tags = arguments.getStringArrayList(HASHTAGS_ARG)!! + } + + isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) CardViewMode.INDENTED else CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + adapter = TimelineAdapter(dataSource, statusDisplayOptions, this) + } + + 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?) { + setupSwipeRefreshLayout() + setupRecyclerView() + updateAdapter() + setupTimelinePreferences() + if (statuses.isEmpty()) { + binding.progressBar.show() + bottomLoading = true + sendInitialRequest() + } else { + binding.progressBar.hide() + if (isNeedRefresh) { + onRefresh() + } + } + } + + private fun sendInitialRequest() { + if (kind == Kind.HOME) { + tryCache() + } else { + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + private fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { statuses: List> -> + val mutableStatusResponse = statuses.toMutableList() + filterStatuses(mutableStatusResponse) + if (statuses.size > 1) { + clearPlaceholdersForResponse(mutableStatusResponse) + this.statuses.clear() + this.statuses.addAll(statuses) + updateAdapter() + binding.progressBar.hide() + // Request statuses including current top to refresh all of them + } + updateCurrent() + loadAbove() + } + } + + private fun updateCurrent() { + if (statuses.isEmpty()) { + return + } + val topId = statuses.first { status -> status.isRight() }!!.asRight().id + timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, + TimelineRequestMode.NETWORK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { statuses: List> -> + + initialUpdateFailed = false + // When cached timeline is too old, we would replace it with nothing + if (statuses.isNotEmpty()) { + val mutableStatuses = statuses.toMutableList() + filterStatuses(mutableStatuses) + if (!this.statuses.isEmpty()) { + // clear old cached statuses + val iterator = this.statuses.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (item.isRight()) { + val (id1) = item.asRight() + if (id1.length < topId.length || id1 < topId) { + iterator.remove() + } + } else { + val (id1) = item.asLeft() + if (id1.length < topId.length || id1 < topId) { + iterator.remove() + } + } + } + } + this.statuses.addAll(mutableStatuses) + updateAdapter() + } + bottomLoading = false + }, + { t: Throwable? -> + Log.d(TAG, "Failed updating timeline", t) + initialUpdateFailed = true + // Indicate that we are not loading anymore + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + }) + } + + private fun setupTimelinePreferences() { + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + if (kind == Kind.HOME) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + filterRemoveReplies = !preferences.getBoolean("tabFilterHomeReplies", true) + filterRemoveReblogs = !preferences.getBoolean("tabFilterHomeBoosts", true) + } + reloadFilters(false) + } + + override fun filterIsRelevant(filter: Filter): Boolean { + return filterContextMatchesKind(kind, filter.context) + } + + override fun refreshAfterApplyingFilters() { + fullyRefresh() + } + + private fun setupSwipeRefreshLayout() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + + private fun setupRecyclerView() { + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this) + { pos -> statuses.getPairedItemOrNull(pos) } + ) + binding.recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = layoutManager + val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + // CWs are expanded without animation, buttons animate itself, we don't need it basically + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter + } + + private fun deleteStatusById(id: String) { + for (i in statuses.indices) { + val either = statuses[i] + if (either.isRight() && id == either.asRight().id) { + statuses.remove(either) + updateAdapter() + break + } + } + if (statuses.isEmpty()) { + showEmptyView() + } + } + + private fun showEmptyView() { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't + * guaranteed to be set until then. */ + scrollListener = if (actionButtonPresent()) { + /* Use a modified scroll listener that both loads more statuses as it goes, and hides + * the follow button on down-scroll. */ + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + hideFab = preferences.getBoolean("fabHide", false) + object : EndlessOnScrollListener(layoutManager) { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(view, dx, dy) + val composeButton = (activity as ActionButtonActivity).actionButton + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown) { + composeButton.hide() // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown) { + composeButton.show() // shows it if we are scrolling up + } + } else if (!composeButton.isShown) { + composeButton.show() + } + } + } + + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + } else { + // Just use the basic scroll listener to load more statuses. + object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + }.also { + binding.recyclerView.addOnScrollListener(it) + } + + if (!eventRegistered) { + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { event: Event? -> + when (event) { + is FavoriteEvent -> handleFavEvent(event) + is ReblogEvent -> handleReblogEvent(event) + is BookmarkEvent -> handleBookmarkEvent(event) + is MuteConversationEvent -> fullyRefresh() + is UnfollowEvent -> { + if (kind == Kind.HOME) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is BlockEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is MuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is DomainMuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val instance = event.instance + removeAllByInstance(instance) + } + } + is StatusDeletedEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.statusId + deleteStatusById(id) + } + } + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(status) + } + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + } + } + eventRegistered = true + } + } + + override fun onRefresh() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.statusView.hide() + isNeedRefresh = false + if (initialUpdateFailed) { + updateCurrent() + } + loadAbove() + } + + private fun loadAbove() { + var firstOrNull: String? = null + var secondOrNull: String? = null + for (i in statuses.indices) { + val status = statuses[i] + if (status.isRight()) { + firstOrNull = status.asRight().id + if (i + 1 < statuses.size && statuses[i + 1].isRight()) { + secondOrNull = statuses[i + 1].asRight().id + } + break + } + } + if (firstOrNull != null) { + sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) + } else { + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + override fun onReply(position: Int) { + super.reply(statuses[position].asRight()) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.reblog(status, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setRebloggedForStatus(position, newStatus, reblog) } + ) { t: Throwable? -> Log.d(TAG, "Failed to reblog status " + status.id, t) } + } + + private fun setRebloggedForStatus(position: Int, status: Status, reblog: Boolean) { + status.reblogged = reblog + if (status.reblog != null) { + status.reblog.reblogged = reblog + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setReblogged(reblog) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.favourite(status, favourite) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setFavouriteForStatus(position, newStatus, favourite) }, + { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } + ) + } + + private fun setFavouriteForStatus(position: Int, status: Status, favourite: Boolean) { + status.favourited = favourite + if (status.reblog != null) { + status.reblog.favourited = favourite + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setFavourited(favourite) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setBookmarkForStatus(position, newStatus, bookmark) }, + { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } + ) + } + + private fun setBookmarkForStatus(position: Int, status: Status, bookmark: Boolean) { + status.bookmarked = bookmark + if (status.reblog != null) { + status.reblog.bookmarked = bookmark + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setBookmarked(bookmark) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onVoteInPoll(position: Int, choices: List) { + val status = statuses[position].asRight() + val votedPoll = status.actionableStatus.poll!!.votedCopy(choices) + setVoteForPoll(position, status, votedPoll) + timelineCases.voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newPoll: Poll -> setVoteForPoll(position, status, newPoll) }, + { t: Throwable? -> Log.d(TAG, "Failed to vote in poll: " + status.id, t) } + ) + } + + private fun setVoteForPoll(position: Int, status: Status, newPoll: Poll) { + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setPoll(newPoll) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onMore(view: View, position: Int) { + super.more(statuses[position].asRight(), view, position) + } + + override fun onOpenReblog(position: Int) { + super.openReblog(statuses[position].asRight()) + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + val newViewData: StatusViewData = StatusViewData.Builder( + statuses.getPairedItem(position) as StatusViewData.Concrete) + .setIsExpanded(expanded).createStatusViewData() + statuses.setPairedItem(position, newViewData) + updateAdapter() + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + val newViewData: StatusViewData = StatusViewData.Builder( + statuses.getPairedItem(position) as StatusViewData.Concrete) + .setIsShowingSensitiveContent(isShowing).createStatusViewData() + statuses.setPairedItem(position, newViewData) + updateAdapter() + } + + override fun onShowReblogs(position: Int) { + val statusId = statuses[position].asRight().id + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val statusId = statuses[position].asRight().id + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onLoadMore(position: Int) { + //check bounds before accessing list, + if (statuses.size >= position && position > 0) { + val fromStatus = statuses[position - 1].asRightOrNull() + val toStatus = statuses[position + 1].asRightOrNull() + val maxMinusOne = if (statuses.size > position + 1 && statuses[position + 2].isRight()) statuses[position + 1].asRight().id else null + if (fromStatus == null || toStatus == null) { + Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + return + } + sendFetchTimelineRequest(fromStatus.id, toStatus.id, maxMinusOne, + FetchEnd.MIDDLE, position) + val (id1) = statuses[position].asLeft() + val newViewData: StatusViewData = StatusViewData.Placeholder(id1, true) + statuses.setPairedItem(position, newViewData) + updateAdapter() + } else { + Log.e(TAG, "error loading more") + } + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + if (position < 0 || position >= statuses.size) { + Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size - 1)) + return + } + val status = statuses.getPairedItem(position) + if (status !is StatusViewData.Concrete) { + // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't + // check for null values when adding values to it although this doesn't seem to be an issue. + Log.e(TAG, String.format( + "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", + status?.javaClass?.simpleName ?: "", + position, + statuses.size - 1 + )) + return + } + val updatedStatus: StatusViewData = StatusViewData.Builder(status) + .setCollapsed(isCollapsed) + .createStatusViewData() + statuses.setPairedItem(position, updatedStatus) + updateAdapter() + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = statuses.getOrNull(position)?.asRightOrNull() ?: return + super.viewMedia(attachmentIndex, status, view) + } + + override fun onViewThread(position: Int) { + super.viewThread(statuses[position].asRight()) + } + + override fun onViewTag(tag: String) { + if (kind == Kind.TAG && tags.size == 1 && tags.contains(tag)) { + // If already viewing a tag page, then ignore any request to view that tag again. + return + } + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id == id) { + /* If already viewing an account page, then any requests to view that account page + * should be ignored. */ + return + } + super.viewAccount(id) + } + + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + when (key) { + PrefKeys.FAB_HIDE -> { + hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + } + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + fullyRefresh() + } + } + PrefKeys.TAB_FILTER_HOME_REPLIES -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) + val oldRemoveReplies = filterRemoveReplies + filterRemoveReplies = kind == Kind.HOME && !filter + if (adapter.itemCount > 1 && oldRemoveReplies != filterRemoveReplies) { + fullyRefresh() + } + } + PrefKeys.TAB_FILTER_HOME_BOOSTS -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) + val oldRemoveReblogs = filterRemoveReblogs + filterRemoveReblogs = kind == Kind.HOME && !filter + if (adapter.itemCount > 1 && oldRemoveReblogs != filterRemoveReblogs) { + fullyRefresh() + } + } + Filter.HOME, Filter.NOTIFICATIONS, Filter.THREAD, Filter.PUBLIC, Filter.ACCOUNT -> { + if (filterContextMatchesKind(kind, listOf(key))) { + reloadFilters(true) + } + } + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> { + //it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + } + } + } + + public override fun removeItem(position: Int) { + statuses.removeAt(position) + updateAdapter() + } + + private fun removeAllByAccountId(accountId: String) { + // using iterator to safely remove items while iterating + val iterator = statuses.iterator() + while (iterator.hasNext()) { + val status = iterator.next().asRightOrNull() + if (status != null && + (status.account.id == accountId || status.actionableStatus.account.id == accountId)) { + iterator.remove() + } + } + updateAdapter() + } + + private fun removeAllByInstance(instance: String) { + // using iterator to safely remove items while iterating + val iterator = statuses.iterator() + while (iterator.hasNext()) { + val status = iterator.next().asRightOrNull() + if (status != null && LinkHelper.getDomain(status.account.url) == instance) { + iterator.remove() + } + } + updateAdapter() + } + + private fun onLoadMore() { + if (didLoadEverythingBottom || bottomLoading) { + return + } + if (statuses.isEmpty()) { + sendInitialRequest() + return + } + bottomLoading = true + val last = statuses[statuses.size - 1] + val placeholder: Placeholder + if (last!!.isRight()) { + val placeholderId = last.asRight().id.dec() + placeholder = Placeholder(placeholderId) + statuses.add(Left(placeholder)) + } else { + placeholder = last.asLeft() + } + statuses.setPairedItem(statuses.size - 1, + StatusViewData.Placeholder(placeholder.id, true)) + updateAdapter() + + val bottomId: String? = if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { + nextId + } else { + statuses.lastOrNull { it.isRight() }?.asRight()?.id + } + + sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1) + } + + private fun fullyRefresh() { + statuses.clear() + updateAdapter() + bottomLoading = true + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + + private fun actionButtonPresent(): Boolean { + return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && + activity is ActionButtonActivity + } + + private fun getFetchCallByTimelineType(fromId: String?, uptoId: String?): Single>> { + val api = mastodonApi + return when (kind) { + Kind.HOME -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) + Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE) + Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE) + Kind.TAG -> { + val firstHashtag = tags[0] + val additionalHashtags = tags.subList(1, tags.size) + api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE) + } + Kind.USER -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, true, null, null) + Kind.USER_PINNED -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, true) + Kind.USER_WITH_REPLIES -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, null) + Kind.FAVOURITES -> api.favourites(fromId, uptoId, LOAD_AT_ONCE) + Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, LOAD_AT_ONCE) + Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, LOAD_AT_ONCE) + else -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) + } + } + + private fun sendFetchTimelineRequest(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, + fetchEnd: FetchEnd, pos: Int) { + if (isAdded && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && binding.progressBar.visibility != View.VISIBLE) && !isSwipeToRefreshEnabled) { + binding.topProgressBar.show() + } + if (kind == Kind.HOME) { + // allow getting old statuses/fallbacks for network only for for bottom loading + val mode = if (fetchEnd == FetchEnd.BOTTOM) { + TimelineRequestMode.ANY + } else { + TimelineRequestMode.NETWORK + } + timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { result: List> -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, + { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } + ) + } else { + getFetchCallByTimelineType(maxId, sinceId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { response: Response> -> + if (response.isSuccessful) { + val 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()!!).toMutableList(), fetchEnd, pos) + } else { + onFetchTimelineFailure(Exception(response.message()), fetchEnd, pos) + } + } + ) { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } + } + } + + private fun extractNextId(response: Response<*>): String? { + val linkHeader = response.headers()["Link"] ?: return null + val links = HttpHeaderLink.parse(linkHeader) + val nextHeader = HttpHeaderLink.findByRelationType(links, "next") ?: return null + val nextLink = nextHeader.uri ?: return null + return nextLink.getQueryParameter("max_id") + } + + private fun onFetchTimelineSuccess(statuses: MutableList>, + fetchEnd: FetchEnd, pos: Int) { + + // We filled the hole (or reached the end) if the server returned less statuses than we + // we asked for. + val fullFetch = statuses.size >= LOAD_AT_ONCE + filterStatuses(statuses) + when (fetchEnd) { + FetchEnd.TOP -> { + updateStatuses(statuses, fullFetch) + } + FetchEnd.MIDDLE -> { + replacePlaceholderWithStatuses(statuses, fullFetch, pos) + } + FetchEnd.BOTTOM -> { + if (!this.statuses.isEmpty() + && !this.statuses[this.statuses.size - 1].isRight()) { + this.statuses.removeAt(this.statuses.size - 1) + updateAdapter() + } + if (statuses.isNotEmpty() && !statuses[statuses.size - 1].isRight()) { + // Removing placeholder if it's the last one from the cache + statuses.removeAt(statuses.size - 1) + } + val oldSize = this.statuses.size + if (this.statuses.size > 1) { + addItems(statuses) + } else { + updateStatuses(statuses, fullFetch) + } + if (this.statuses.size == oldSize) { + // This may be a brittle check but seems like it works + // Can we check it using headers somehow? Do all server support them? + didLoadEverythingBottom = true + } + } + } + if (isAdded) { + binding.topProgressBar.hide() + updateBottomLoadingState(fetchEnd) + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.isEnabled = true + if (this.statuses.size == 0) { + showEmptyView() + } else { + binding.statusView.hide() + } + } + } + + private fun onFetchTimelineFailure(throwable: Throwable, fetchEnd: FetchEnd, position: Int) { + if (isAdded) { + binding.swipeRefreshLayout.isRefreshing = false + binding.topProgressBar.hide() + if (fetchEnd == FetchEnd.MIDDLE && !statuses[position].isRight()) { + var placeholder = statuses[position].asLeftOrNull() + val newViewData: StatusViewData + if (placeholder == null) { + val (id1) = statuses[position - 1].asRight() + val newId = id1.dec() + placeholder = Placeholder(newId) + } + newViewData = StatusViewData.Placeholder(placeholder.id, false) + statuses.setPairedItem(position, newViewData) + updateAdapter() + } else if (statuses.isEmpty()) { + binding.swipeRefreshLayout.isEnabled = false + binding.statusView.visibility = View.VISIBLE + if (throwable is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.progressBar.visibility = View.VISIBLE + onRefresh() + } + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.visibility = View.VISIBLE + onRefresh() + } + } + } + Log.e(TAG, "Fetch Failure: " + throwable.message) + updateBottomLoadingState(fetchEnd) + binding.progressBar.hide() + } + } + + private fun updateBottomLoadingState(fetchEnd: FetchEnd) { + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false + } + } + + private fun filterStatuses(statuses: MutableList>) { + val it = statuses.iterator() + while (it.hasNext()) { + val status = it.next().asRightOrNull() + if (status != null + && (status.inReplyToId != null && filterRemoveReplies + || status.reblog != null && filterRemoveReblogs + || shouldFilterStatus(status.actionableStatus))) { + it.remove() + } + } + } + + private fun updateStatuses(newStatuses: MutableList>, fullFetch: Boolean) { + if (newStatuses.isEmpty()) { + updateAdapter() + return + } + if (statuses.isEmpty()) { + statuses.addAll(newStatuses) + } else { + val lastOfNew = newStatuses[newStatuses.size - 1] + val index = statuses.indexOf(lastOfNew) + if (index >= 0) { + statuses.subList(0, index).clear() + } + val newIndex = newStatuses.indexOf(statuses[0]) + if (newIndex == -1) { + if (index == -1 && fullFetch) { + val placeholderId = newStatuses.last { status -> status.isRight() }.asRight().id.inc() + newStatuses.add(Left(Placeholder(placeholderId))) + } + statuses.addAll(0, newStatuses) + } else { + statuses.addAll(0, newStatuses.subList(0, newIndex)) + } + } + // Remove all consecutive placeholders + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun removeConsecutivePlaceholders() { + for (i in 0 until statuses.size - 1) { + if (statuses[i].isLeft() && statuses[i + 1].isLeft()) { + statuses.removeAt(i) + } + } + } + + private fun addItems(newStatuses: List?>) { + if (newStatuses.isEmpty()) { + return + } + val last = statuses.last { status -> + status.isRight() + } + + // I was about to replace findStatus with indexOf but it is incorrect to compare value + // types by ID anyway and we should change equals() for Status, I think, so this makes sense + if (last != null && !newStatuses.contains(last)) { + statuses.addAll(newStatuses) + removeConsecutivePlaceholders() + updateAdapter() + } + } + + /** + * For certain requests we don't want to see placeholders, they will be removed some other way + */ + private fun clearPlaceholdersForResponse(statuses: MutableList>) { + statuses.removeAll{ status -> status.isLeft() } + } + + private fun replacePlaceholderWithStatuses(newStatuses: MutableList>, + fullFetch: Boolean, pos: Int) { + val placeholder = statuses[pos] + if (placeholder.isLeft()) { + statuses.removeAt(pos) + } + if (newStatuses.isEmpty()) { + updateAdapter() + return + } + if (fullFetch) { + newStatuses.add(placeholder) + } + statuses.addAll(pos, newStatuses) + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun findStatusOrReblogPositionById(statusId: String): Int { + return statuses.indexOfFirst { either -> + val status = either.asRightOrNull() + status != null && + (statusId == status.id || + (status.reblog != null && statusId == status.reblog.id)) + } + } + + private val statusLifter: Function1> = { value -> Right(value) } + + private fun findStatusAndPosition(position: Int, status: Status): Pair? { + val statusToUpdate: StatusViewData.Concrete + val positionToUpdate: Int + val someOldViewData = statuses.getPairedItem(position) + + // Unlikely, but data could change between the request and response + if (someOldViewData is StatusViewData.Placeholder || + (someOldViewData as StatusViewData.Concrete).id != status.id) { + // try to find the status we need to update + val foundPos = statuses.indexOf(Right(status)) + if (foundPos < 0) return null // okay, it's hopeless, give up + statusToUpdate = statuses.getPairedItem(foundPos) as StatusViewData.Concrete + positionToUpdate = position + } else { + statusToUpdate = someOldViewData + positionToUpdate = position + } + return Pair(statusToUpdate, positionToUpdate) + } + + private fun handleReblogEvent(reblogEvent: ReblogEvent) { + val pos = findStatusOrReblogPositionById(reblogEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setRebloggedForStatus(pos, status, reblogEvent.reblog) + } + + private fun handleFavEvent(favEvent: FavoriteEvent) { + val pos = findStatusOrReblogPositionById(favEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setFavouriteForStatus(pos, status, favEvent.favourite) + } + + private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) { + val pos = findStatusOrReblogPositionById(bookmarkEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setBookmarkForStatus(pos, status, bookmarkEvent.bookmark) + } + + private fun handleStatusComposeEvent(status: Status) { + when (kind) { + Kind.HOME, Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL -> onRefresh() + Kind.USER, Kind.USER_WITH_REPLIES -> if (status.account.id == id) { + onRefresh() + } else { + return + } + Kind.TAG, Kind.FAVOURITES, Kind.LIST, Kind.BOOKMARKS, Kind.USER_PINNED -> return + } + } + + private fun liftStatusList(list: List): List> { + return list.map(statusLifter) + } + + private fun updateAdapter() { + differ.submitList(statuses.pairedCopy) + } + + private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (isAdded) { + adapter.notifyItemRangeInserted(position, count) + val context = context + // scroll up when new items at the top are loaded while being in the first position + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.itemCount != count) { + if (isSwipeToRefreshEnabled) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30)) + } else binding.recyclerView.scrollToPosition(0) + } + } + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position, count, payload) + } + } + private val differ = AsyncListDiffer(listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build()) + + private val dataSource: TimelineAdapter.AdapterDataSource = object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): StatusViewData { + return differ.currentList[pos] + } + } + + private var talkBackWasEnabled = false + + override fun onResume() { + super.onResume() + val a11yManager = ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java) + + val wasEnabled = talkBackWasEnabled + talkBackWasEnabled = a11yManager?.isEnabled == true + Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") + if (talkBackWasEnabled && !wasEnabled) { + adapter.notifyDataSetChanged() + } + startUpdateTimestamp() + } + + /** + * Start to update adapter every minute to refresh timestamp + * If setting absoluteTimeView is false + * Auto dispose observable on pause + */ + private fun startUpdateTimestamp() { + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_PAUSE)) + .subscribe { updateAdapter() } + } + } + + override fun onReselect() { + if (isAdded) { + layoutManager!!.scrollToPosition(0) + binding.recyclerView.stopScroll() + scrollListener!!.reset() + } + } + + override fun refreshContent() { + if (isAdded) { + onRefresh() + } else { + isNeedRefresh = true + } + } + + enum class Kind { + HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS + } + + private enum class FetchEnd { + TOP, BOTTOM, MIDDLE + } + + companion object { + private const val TAG = "TimelineF" // logging tag + private const val KIND_ARG = "kind" + private const val ID_ARG = "id" + private const val HASHTAGS_ARG = "hashtags" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" + private const val LOAD_AT_ONCE = 30 + + fun newInstance(kind: Kind, hashtagOrId: String? = null, enableSwipeToRefresh: Boolean = true): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, kind.name) + arguments.putString(ID_ARG, hashtagOrId) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newHashtagInstance(hashtags: List): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, Kind.TAG.name) + arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags)) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + fragment.arguments = arguments + return fragment + } + + private fun filterContextMatchesKind(kind: Kind?, filterContext: List): Boolean { + // home, notifications, public, thread + return when (kind) { + Kind.HOME, Kind.LIST -> filterContext.contains(Filter.HOME) + Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL, Kind.TAG -> filterContext.contains(Filter.PUBLIC) + Kind.FAVOURITES -> filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS) + Kind.USER, Kind.USER_WITH_REPLIES, Kind.USER_PINNED -> filterContext.contains(Filter.ACCOUNT) + else -> false + } + } + + private val diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { + return oldItem.viewDataId == newItem.viewDataId + } + + override fun areContentsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: StatusViewData, newItem: StatusViewData): Any? { + return if (oldItem.deepEquals(newItem)) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else // If items are different - update the whole view holder + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 8594dfc60..859162da3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -22,7 +22,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData import kotlin.math.min // Not using lambdas because there's boxing of int then -interface StatusProvider { +fun interface StatusProvider { fun getStatus(pos: Int): StatusViewData? }