From 16499fb7cd6d4fd6892d56bfb53d5ed637991fce Mon Sep 17 00:00:00 2001 From: Adolfo Santiago Date: Tue, 24 Aug 2021 11:09:11 +0200 Subject: [PATCH] Init commit --- .github/FUNDING.yml | 1 + .gitignore | 9 + .travis.yml | 35 + CONTRIBUTING.md | 45 + ISSUE_TEMPLATE.md | 9 + LICENSE.txt | 674 +++++++ README.md | 37 + app/.gitignore | 2 + app/build.gradle | 206 +++ app/proguard-rules.pro | 71 + .../10.json | 275 +++ .../11.json | 515 ++++++ .../12.json | 668 +++++++ .../13.json | 656 +++++++ .../14.json | 662 +++++++ .../15.json | 674 +++++++ .../16.json | 680 +++++++ .../17.json | 686 +++++++ .../18.json | 693 +++++++ .../19.json | 711 +++++++ .../20.json | 723 ++++++++ .../21.json | 735 ++++++++ .../22.json | 741 ++++++++ .../23.json | 753 ++++++++ .../24.json | 759 ++++++++ .../25.json | 897 +++++++++ .../26.json | 909 +++++++++ .../27.json | 989 ++++++++++ .../tusky/ExampleInstrumentedTest.java | 0 .../com/keylesspalace/tusky/MigrationsTest.kt | 64 + .../keylesspalace/tusky/TimelineDAOTest.kt | 249 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + app/src/blue/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5329 bytes .../res/mipmap-hdpi/ic_shortcut_compose.png | Bin 0 -> 3522 bytes app/src/blue/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2839 bytes .../res/mipmap-mdpi/ic_shortcut_compose.png | Bin 0 -> 1837 bytes app/src/blue/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6418 bytes .../res/mipmap-xhdpi/ic_shortcut_compose.png | Bin 0 -> 4382 bytes .../blue/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 12200 bytes .../res/mipmap-xxhdpi/ic_shortcut_compose.png | Bin 0 -> 8414 bytes .../blue/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14987 bytes .../mipmap-xxxhdpi/ic_shortcut_compose.png | Bin 0 -> 10507 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + app/src/green/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5389 bytes app/src/green/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2865 bytes .../green/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6540 bytes .../green/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 12355 bytes .../green/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 15128 bytes .../husky/res/values-ar/husky_generated.xml | 56 + app/src/husky/res/values-ar/strings.xml | 2 + .../husky/res/values-ber/husky_generated.xml | 14 + .../res/values-bn-rBD/husky_generated.xml | 62 + .../res/values-bn-rIN/husky_generated.xml | 56 + .../husky/res/values-ca/husky_generated.xml | 63 + app/src/husky/res/values-ca/strings.xml | 29 + .../husky/res/values-ckb/husky_generated.xml | 56 + .../husky/res/values-cs/husky_generated.xml | 62 + .../husky/res/values-cy/husky_generated.xml | 53 + .../husky/res/values-de/husky_generated.xml | 59 + app/src/husky/res/values-de/strings.xml | 69 + .../res/values-en-rAU/husky_generated.xml | 7 + .../res/values-en-rGB/husky_generated.xml | 7 + app/src/husky/res/values-en-rGB/strings.xml | 6 + .../husky/res/values-eo/husky_generated.xml | 58 + .../husky/res/values-es/husky_generated.xml | 62 + app/src/husky/res/values-es/strings.xml | 71 + .../husky/res/values-eu/husky_generated.xml | 59 + .../husky/res/values-fa/husky_generated.xml | 56 + .../husky/res/values-fr/husky_generated.xml | 62 + app/src/husky/res/values-fr/strings.xml | 21 + .../husky/res/values-ga/husky_generated.xml | 56 + .../husky/res/values-gd/husky_generated.xml | 11 + .../husky/res/values-hi/husky_generated.xml | 46 + app/src/husky/res/values-hi/strings.xml | 7 + .../husky/res/values-hu/husky_generated.xml | 57 + .../husky/res/values-is/husky_generated.xml | 56 + .../husky/res/values-it/husky_generated.xml | 58 + .../husky/res/values-ja/husky_generated.xml | 57 + app/src/husky/res/values-ja/strings.xml | 24 + .../husky/res/values-kab/husky_generated.xml | 29 + .../husky/res/values-ko/husky_generated.xml | 50 + .../husky/res/values-ml/husky_generated.xml | 11 + app/src/husky/res/values-nb-rNO/strings.xml | 68 + .../husky/res/values-nl/husky_generated.xml | 56 + app/src/husky/res/values-nn/strings.xml | 4 + .../res/values-no-rNB/husky_generated.xml | 56 + .../husky/res/values-oc/husky_generated.xml | 60 + .../husky/res/values-pa/husky_generated.xml | 7 + .../husky/res/values-pl/husky_generated.xml | 58 + app/src/husky/res/values-pl/strings.xml | 77 + .../res/values-pt-rBR/husky_generated.xml | 58 + app/src/husky/res/values-pt-rBR/strings.xml | 68 + app/src/husky/res/values-pt-rPT/strings.xml | 2 + .../husky/res/values-ru/husky_generated.xml | 58 + app/src/husky/res/values-ru/strings.xml | 49 + .../husky/res/values-sa/husky_generated.xml | 56 + .../husky/res/values-sk/husky_generated.xml | 11 + .../husky/res/values-sl/husky_generated.xml | 53 + .../husky/res/values-sv/husky_generated.xml | 59 + app/src/husky/res/values-sv/strings.xml | 69 + .../husky/res/values-ta/husky_generated.xml | 50 + .../husky/res/values-te/husky_generated.xml | 7 + .../husky/res/values-th/husky_generated.xml | 56 + app/src/husky/res/values-th/strings.xml | 68 + .../husky/res/values-tr/husky_generated.xml | 56 + app/src/husky/res/values-tr/strings.xml | 68 + .../husky/res/values-uk/husky_generated.xml | 11 + app/src/husky/res/values-uk/strings.xml | 21 + .../husky/res/values-vi/husky_generated.xml | 56 + .../res/values-zh-rCN/husky_generated.xml | 60 + .../res/values-zh-rHK/husky_generated.xml | 45 + .../res/values-zh-rMO/husky_generated.xml | 45 + .../res/values-zh-rSG/husky_generated.xml | 51 + .../res/values-zh-rTW/husky_generated.xml | 48 + app/src/husky/res/values/donottranslate.xml | 9 + .../husky/res/values/husky_donottranslate.xml | 19 + app/src/husky/res/values/husky_generated.xml | 64 + app/src/husky/res/values/strings.xml | 166 ++ app/src/main/AndroidManifest.xml | 193 ++ app/src/main/ic_bbcode.svg | 74 + app/src/main/ic_html.svg | 74 + app/src/main/ic_launcher-web.png | Bin 0 -> 44818 bytes app/src/main/ic_launcher.svg | 146 ++ app/src/main/ic_launcher_.svg | 121 ++ app/src/main/ic_launcher_foreground.svg | 107 ++ app/src/main/ic_sticker.svg | 65 + .../com/keylesspalace/tusky/AboutActivity.kt | 87 + .../keylesspalace/tusky/AccountActivity.kt | 974 ++++++++++ .../tusky/AccountListActivity.kt | 105 ++ .../tusky/AccountsInListFragment.kt | 285 +++ .../com/keylesspalace/tusky/BaseActivity.java | 222 +++ .../tusky/BottomSheetActivity.kt | 225 +++ .../tusky/EditProfileActivity.kt | 427 +++++ .../keylesspalace/tusky/FiltersActivity.kt | 219 +++ .../keylesspalace/tusky/LicenseActivity.kt | 84 + .../com/keylesspalace/tusky/ListsActivity.kt | 287 +++ .../com/keylesspalace/tusky/LoginActivity.kt | 389 ++++ .../com/keylesspalace/tusky/MainActivity.kt | 836 +++++++++ .../tusky/ModalTimelineActivity.kt | 69 + .../tusky/SavedTootActivity.java | 222 +++ .../com/keylesspalace/tusky/SplashActivity.kt | 50 + .../keylesspalace/tusky/StatusListActivity.kt | 96 + .../java/com/keylesspalace/tusky/TabData.kt | 112 ++ .../tusky/TabPreferenceActivity.kt | 372 ++++ .../keylesspalace/tusky/TuskyApplication.kt | 114 ++ .../keylesspalace/tusky/ViewMediaActivity.kt | 388 ++++ .../keylesspalace/tusky/ViewTagActivity.java | 91 + .../tusky/ViewThreadActivity.java | 130 ++ .../tusky/adapter/AccountAdapter.java | 112 ++ .../tusky/adapter/AccountFieldAdapter.kt | 78 + .../tusky/adapter/AccountFieldEditAdapter.kt | 98 + .../tusky/adapter/AccountSelectionAdapter.kt | 57 + .../tusky/adapter/AccountViewHolder.java | 64 + .../tusky/adapter/AddPollOptionsAdapter.kt | 92 + .../tusky/adapter/BlocksAdapter.java | 109 ++ .../tusky/adapter/ChatMessagesAdapter.kt | 229 +++ .../tusky/adapter/ChatsAdapter.kt | 209 +++ .../tusky/adapter/EmojiAdapter.kt | 64 + .../tusky/adapter/EmojiReactionsAdapter.java | 72 + .../tusky/adapter/FollowAdapter.java | 61 + .../tusky/adapter/FollowRequestViewHolder.kt | 58 + .../tusky/adapter/FollowRequestsAdapter.java | 60 + .../tusky/adapter/HashtagViewHolder.kt | 16 + .../tusky/adapter/ListSelectionAdapter.kt | 41 + .../tusky/adapter/LoadingFooterViewHolder.kt | 21 + .../tusky/adapter/MutedStatusViewHolder.java | 170 ++ .../tusky/adapter/MutesAdapter.java | 135 ++ .../tusky/adapter/NetworkStateViewHolder.kt | 45 + .../tusky/adapter/NotificationsAdapter.java | 734 ++++++++ .../tusky/adapter/PlaceholderViewHolder.java | 60 + .../tusky/adapter/PollAdapter.kt | 130 ++ .../adapter/PreviewPollOptionsAdapter.kt | 67 + .../tusky/adapter/SavedTootAdapter.java | 122 ++ .../tusky/adapter/SingleViewHolder.java | 11 + .../tusky/adapter/StatusBaseViewHolder.java | 1166 ++++++++++++ .../adapter/StatusDetailedViewHolder.java | 193 ++ .../tusky/adapter/StatusViewHolder.java | 132 ++ .../tusky/adapter/StickerAdapater.kt | 117 ++ .../keylesspalace/tusky/adapter/TabAdapter.kt | 153 ++ .../tusky/adapter/ThreadAdapter.java | 164 ++ .../tusky/adapter/TimelineAdapter.java | 151 ++ .../tusky/adapter/UnicodeEmojiAdapter.java | 129 ++ .../tusky/appstore/CacheUpdater.kt | 59 + .../keylesspalace/tusky/appstore/Events.kt | 28 + .../keylesspalace/tusky/appstore/EventsHub.kt | 22 + .../announcements/AnnouncementAdapter.kt | 126 ++ .../announcements/AnnouncementsActivity.kt | 180 ++ .../announcements/AnnouncementsViewModel.kt | 188 ++ .../tusky/components/chat/ChatActivity.kt | 1125 +++++++++++ .../tusky/components/chat/ChatViewModel.kt | 29 + .../common/CommonComposeViewModel.kt | 382 ++++ .../components/common/DownsizeImageTask.java | 154 ++ .../tusky/components/common/MediaUploader.kt | 261 +++ .../components/compose/ComposeActivity.kt | 1370 ++++++++++++++ .../compose/ComposeAutoCompleteAdapter.java | 320 ++++ .../components/compose/ComposeViewModel.kt | 295 +++ .../components/compose/MediaPreviewAdapter.kt | 159 ++ .../compose/dialog/AddPollDialog.kt | 101 + .../compose/dialog/CaptionDialog.kt | 118 ++ .../compose/view/ComposeOptionsView.kt | 70 + .../compose/view/ComposeScheduleView.java | 228 +++ .../components/compose/view/EditTextTyped.kt | 65 + .../compose/view/PollPreviewView.kt | 64 + .../compose/view/ProgressImageView.java | 122 ++ .../compose/view/ProgressTextView.java | 121 ++ .../components/compose/view/TootButton.kt | 75 + .../conversation/ConversationAdapter.kt | 110 ++ .../conversation/ConversationEntity.kt | 194 ++ .../conversation/ConversationViewHolder.java | 168 ++ .../ConversationsBoundaryCallback.kt | 98 + .../conversation/ConversationsFragment.kt | 204 ++ .../conversation/ConversationsRepository.kt | 111 ++ .../conversation/ConversationsViewModel.kt | 142 ++ .../tusky/components/drafts/DraftHelper.kt | 161 ++ .../components/drafts/DraftMediaAdapter.kt | 81 + .../tusky/components/drafts/DraftsActivity.kt | 199 ++ .../tusky/components/drafts/DraftsAdapter.kt | 92 + .../components/drafts/DraftsViewModel.kt | 69 + .../instancemute/InstanceListActivity.kt | 47 + .../adapter/DomainMutesAdapter.kt | 57 + .../fragment/InstanceListFragment.kt | 179 ++ .../interfaces/InstanceActionListener.kt | 5 + .../notifications/NotificationFetcher.kt | 83 + .../notifications/NotificationHelper.java | 763 ++++++++ .../notifications/NotificationWorker.kt | 51 + .../components/notifications/Notifier.kt | 20 + .../preference/AccountPreferencesFragment.kt | 390 ++++ .../components/preference/EmojiPreference.kt | 258 +++ .../NotificationPreferencesFragment.kt | 213 +++ .../preference/PreferencesActivity.kt | 192 ++ .../preference/PreferencesFragment.kt | 340 ++++ .../preference/ProxyPreferencesFragment.kt | 69 + .../TabFilterPreferencesFragment.kt | 54 + .../tusky/components/report/ReportActivity.kt | 150 ++ .../components/report/ReportViewModel.kt | 225 +++ .../tusky/components/report/Screen.kt | 24 + .../report/adapter/AdapterHandler.kt | 28 + .../report/adapter/ReportPagerAdapter.kt | 36 + .../report/adapter/StatusViewHolder.kt | 178 ++ .../report/adapter/StatusesAdapter.kt | 65 + .../report/adapter/StatusesDataSource.kt | 150 ++ .../adapter/StatusesDataSourceFactory.kt | 36 + .../report/adapter/StatusesRepository.kt | 61 + .../report/fragments/ReportDoneFragment.kt | 96 + .../report/fragments/ReportNoteFragment.kt | 128 ++ .../fragments/ReportStatusesFragment.kt | 200 ++ .../report/model/StatusViewState.kt | 36 + .../scheduled/ScheduledTootActivity.kt | 149 ++ .../scheduled/ScheduledTootAdapter.kt | 85 + .../scheduled/ScheduledTootDataSource.kt | 102 + .../scheduled/ScheduledTootViewModel.kt | 68 + .../tusky/components/search/SearchActivity.kt | 126 ++ .../tusky/components/search/SearchType.kt | 7 + .../components/search/SearchViewModel.kt | 242 +++ .../search/adapter/SearchAccountsAdapter.kt | 58 + .../search/adapter/SearchDataSource.kt | 126 ++ .../search/adapter/SearchDataSourceFactory.kt | 44 + .../search/adapter/SearchHashtagsAdapter.kt | 55 + .../search/adapter/SearchPagerAdapter.kt | 38 + .../search/adapter/SearchRepository.kt | 56 + .../search/adapter/SearchStatusesAdapter.kt | 63 + .../fragments/SearchAccountsFragment.kt | 39 + .../search/fragments/SearchFragment.kt | 132 ++ .../fragments/SearchHashtagsFragment.kt | 38 + .../fragments/SearchStatusesFragment.kt | 517 ++++++ .../com/keylesspalace/tusky/db/AccountDao.kt | 31 + .../keylesspalace/tusky/db/AccountEntity.kt | 90 + .../keylesspalace/tusky/db/AccountManager.kt | 203 ++ .../keylesspalace/tusky/db/AppDatabase.java | 408 ++++ .../com/keylesspalace/tusky/db/ChatEntity.kt | 21 + .../tusky/db/ChatMessageEntity.kt | 21 + .../com/keylesspalace/tusky/db/ChatsDao.kt | 84 + .../tusky/db/ConversationsDao.kt | 41 + .../com/keylesspalace/tusky/db/Converters.kt | 170 ++ .../com/keylesspalace/tusky/db/DraftDao.kt | 40 + .../com/keylesspalace/tusky/db/DraftEntity.kt | 56 + .../com/keylesspalace/tusky/db/InstanceDao.kt | 31 + .../keylesspalace/tusky/db/InstanceEntity.kt | 33 + .../com/keylesspalace/tusky/db/TimelineDao.kt | 111 ++ .../tusky/db/TimelineStatusEntity.kt | 81 + .../com/keylesspalace/tusky/db/TootDao.java | 45 + .../keylesspalace/tusky/db/TootEntity.java | 170 ++ .../tusky/di/ActivitiesModule.kt | 118 ++ .../keylesspalace/tusky/di/AppComponent.kt | 52 + .../com/keylesspalace/tusky/di/AppInjector.kt | 80 + .../com/keylesspalace/tusky/di/AppModule.kt | 91 + .../tusky/di/BroadcastReceiverModule.kt | 31 + .../tusky/di/FragmentBuildersModule.kt | 95 + .../com/keylesspalace/tusky/di/GlideModule.kt | 12 + .../com/keylesspalace/tusky/di/Injectable.kt | 23 + .../tusky/di/MediaUploaderModule.kt | 30 + .../keylesspalace/tusky/di/NetworkModule.kt | 89 + .../tusky/di/RepositoryModule.kt | 35 + .../keylesspalace/tusky/di/ServicesModule.kt | 43 + .../tusky/di/ViewModelFactory.kt | 107 ++ .../keylesspalace/tusky/entity/AccessToken.kt | 22 + .../com/keylesspalace/tusky/entity/Account.kt | 105 ++ .../tusky/entity/Announcement.kt | 57 + .../tusky/entity/AppCredentials.kt | 23 + .../keylesspalace/tusky/entity/Attachment.kt | 95 + .../com/keylesspalace/tusky/entity/Card.kt | 45 + .../com/keylesspalace/tusky/entity/Chat.kt | 44 + .../tusky/entity/Conversation.kt | 25 + .../tusky/entity/DeletedStatus.kt | 34 + .../com/keylesspalace/tusky/entity/Emoji.kt | 36 + .../com/keylesspalace/tusky/entity/Filter.kt | 48 + .../com/keylesspalace/tusky/entity/HashTag.kt | 3 + .../tusky/entity/IdentityProof.kt | 9 + .../keylesspalace/tusky/entity/Instance.kt | 70 + .../com/keylesspalace/tusky/entity/Marker.kt | 15 + .../keylesspalace/tusky/entity/MastoList.kt | 26 + .../keylesspalace/tusky/entity/NewStatus.kt | 40 + .../keylesspalace/tusky/entity/NodeInfo.kt | 63 + .../tusky/entity/Notification.kt | 107 ++ .../com/keylesspalace/tusky/entity/Poll.kt | 47 + .../tusky/entity/Relationship.kt | 33 + .../tusky/entity/ScheduledStatus.kt | 25 + .../tusky/entity/SearchResult.kt | 22 + .../com/keylesspalace/tusky/entity/Status.kt | 214 +++ .../tusky/entity/StatusContext.kt | 21 + .../tusky/entity/StatusParams.kt | 26 + .../com/keylesspalace/tusky/entity/Sticker.kt | 30 + .../keylesspalace/tusky/entity/StreamEvent.kt | 20 + .../tusky/fragment/AccountListFragment.kt | 417 +++++ .../tusky/fragment/AccountMediaFragment.kt | 356 ++++ .../tusky/fragment/BaseFragment.java | 43 + .../tusky/fragment/ChatsFragment.kt | 781 ++++++++ .../tusky/fragment/NotificationsFragment.java | 1484 +++++++++++++++ .../tusky/fragment/SFragment.java | 655 +++++++ .../tusky/fragment/TimePickerFragment.java | 53 + .../tusky/fragment/TimelineFragment.java | 1648 +++++++++++++++++ .../tusky/fragment/ViewImageFragment.kt | 288 +++ .../tusky/fragment/ViewMediaFragment.kt | 95 + .../tusky/fragment/ViewThreadFragment.java | 821 ++++++++ .../tusky/fragment/ViewVideoFragment.kt | 207 +++ .../interfaces/AccountActionListener.java | 23 + .../interfaces/AccountSelectionListener.kt | 22 + .../interfaces/ActionButtonActivity.java | 28 + .../tusky/interfaces/ChatActionListener.kt | 11 + .../tusky/interfaces/LinkListener.java | 22 + .../tusky/interfaces/PermissionRequester.java | 5 + .../tusky/interfaces/RefreshableFragment.kt | 11 + .../tusky/interfaces/ReselectableFragment.kt | 11 + .../interfaces/StatusActionListener.java | 72 + .../tusky/json/SpannedTypeAdapter.kt | 37 + .../InstanceSwitchAuthInterceptor.java | 76 + .../tusky/network/MastodonApi.kt | 684 +++++++ .../tusky/network/ProgressRequestBody.java | 74 + .../tusky/network/TimelineCases.kt | 168 ++ .../tusky/pager/AccountPagerAdapter.kt | 55 + .../tusky/pager/AvatarImagePagerAdapter.kt | 25 + .../tusky/pager/ImagePagerAdapter.kt | 42 + .../tusky/pager/MainPagerAdapter.kt | 32 + .../NotificationClearBroadcastReceiver.kt | 44 + .../receiver/SendStatusBroadcastReceiver.kt | 167 ++ .../tusky/repository/ChatRepository.kt | 264 +++ .../tusky/repository/TimelineRepository.kt | 393 ++++ .../tusky/service/SendTootService.kt | 435 +++++ .../tusky/service/ServiceClient.kt | 46 + .../tusky/service/StreamingService.kt | 239 +++ .../tusky/service/TuskyTileService.kt | 39 + .../tusky/settings/SettingsConstants.kt | 74 + .../tusky/settings/SettingsDSL.kt | 84 + .../keylesspalace/tusky/util/BBCodeEdit.java | 83 + .../com/keylesspalace/tusky/util/BiListing.kt | 38 + .../tusky/util/BindingViewHolder.kt | 8 + .../tusky/util/BlurHashDecoder.kt | 130 ++ .../keylesspalace/tusky/util/CardViewMode.kt | 7 + .../tusky/util/ClickableSpanNoUnderline.kt | 11 + .../tusky/util/ComposeTokenizer.kt | 108 ++ .../tusky/util/CustomEmojiHelper.kt | 159 ++ .../tusky/util/CustomFragmentStateAdapter.kt | 28 + .../tusky/util/CustomURLSpan.java | 41 + .../com/keylesspalace/tusky/util/Either.kt | 47 + .../tusky/util/EmojiCompatFont.kt | 355 ++++ .../com/keylesspalace/tusky/util/Emojis.java | 1084 +++++++++++ .../tusky/util/FocalPointUtil.kt | 157 ++ .../keylesspalace/tusky/util/HTMLEdit.java | 104 ++ .../tusky/util/HttpHeaderLink.java | 162 ++ .../com/keylesspalace/tusky/util/IOUtils.java | 71 + .../tusky/util/ImageLoadingHelper.kt | 51 + .../keylesspalace/tusky/util/LinkHelper.java | 265 +++ .../util/ListStatusAccessibilityDelegate.kt | 325 ++++ .../com/keylesspalace/tusky/util/ListUtils.kt | 55 + .../com/keylesspalace/tusky/util/Listing.kt | 36 + .../keylesspalace/tusky/util/LiveDataUtil.kt | 111 ++ .../keylesspalace/tusky/util/LocaleManager.kt | 41 + .../keylesspalace/tusky/util/MediaUtils.kt | 230 +++ .../keylesspalace/tusky/util/NetworkState.kt | 34 + .../tusky/util/NotificationTypeConverter.kt | 45 + .../keylesspalace/tusky/util/OkHttpUtils.java | 89 + .../tusky/util/OmittedDomainFetcher.kt | 58 + .../tusky/util/PagingRequestHelper.java | 491 +++++ .../keylesspalace/tusky/util/PairedList.java | 94 + .../com/keylesspalace/tusky/util/Resource.kt | 13 + .../tusky/util/RxAwareViewModel.kt | 18 + .../tusky/util/SaveTootHelper.java | 57 + .../tusky/util/ShareShortcutHelper.kt | 103 ++ .../tusky/util/SharedPreferencesExtensions.kt | 7 + .../tusky/util/SmartLengthInputFilter.kt | 111 ++ .../com/keylesspalace/tusky/util/SpanUtils.kt | 161 ++ .../tusky/util/StatusDisplayOptions.kt | 22 + .../tusky/util/StatusViewHelper.kt | 331 ++++ .../keylesspalace/tusky/util/StringUtils.kt | 91 + .../keylesspalace/tusky/util/ThemeUtils.java | 83 + .../tusky/util/TimestampUtils.java | 105 ++ .../tusky/util/VersionUtils.java | 49 + .../tusky/util/ViewDataUtils.java | 128 ++ .../tusky/util/ViewExtensions.kt | 115 ++ .../tusky/util/ViewPager2Fix.java | 40 + .../tusky/util/getErrorMessage.kt | 23 + .../tusky/view/BackgroundMessageView.kt | 46 + .../tusky/view/BezelImageView.java | 61 + .../view/ConversationLineItemDecoration.kt | 73 + .../tusky/view/CustomEmojiTextView.kt | 60 + .../tusky/view/EmojiKeyboard.java | 153 ++ .../keylesspalace/tusky/view/EmojiPicker.kt | 17 + .../tusky/view/EndlessOnScrollListener.java | 54 + .../tusky/view/ExposedPlayPauseVideoView.kt | 33 + .../keylesspalace/tusky/view/LicenseCard.kt | 58 + .../tusky/view/MediaPreviewImageView.kt | 129 ++ .../tusky/view/MuteAccountDialog.kt | 32 + .../tusky/view/SquareImageView.kt | 24 + .../keylesspalace/tusky/view/StatusView.kt | 75 + .../tusky/viewdata/AttachmentViewData.kt | 30 + .../tusky/viewdata/ChatViewData.kt | 135 ++ .../tusky/viewdata/NotificationViewData.java | 144 ++ .../tusky/viewdata/PollViewData.kt | 80 + .../tusky/viewdata/StatusViewData.java | 765 ++++++++ .../tusky/viewmodel/AccountViewModel.kt | 313 ++++ .../viewmodel/AccountsInListViewModel.kt | 92 + .../tusky/viewmodel/EditProfileViewModel.kt | 288 +++ .../tusky/viewmodel/ListsViewModel.kt | 111 ++ app/src/main/res/anim/explode.xml | 12 + app/src/main/res/anim/fade_in.xml | 6 + app/src/main/res/anim/fade_out.xml | 6 + app/src/main/res/anim/slide_from_left.xml | 6 + app/src/main/res/anim/slide_from_right.xml | 6 + app/src/main/res/anim/slide_to_left.xml | 6 + app/src/main/res/anim/slide_to_right.xml | 6 + .../main/res/color/account_tab_font_color.xml | 5 + .../color/color_background_transparent_60.xml | 4 + .../main/res/color/compound_button_color.xml | 6 + .../main/res/color/emoji_reaction_button.xml | 5 + .../text_input_layout_box_stroke_color.xml | 5 + .../main/res/drawable-hdpi/elephant_error.png | Bin 0 -> 31252 bytes .../res/drawable-hdpi/elephant_friend.png | Bin 0 -> 32811 bytes .../drawable-hdpi/elephant_friend_empty.png | Bin 0 -> 41554 bytes .../res/drawable-hdpi/elephant_offline.png | Bin 0 -> 31041 bytes app/src/main/res/drawable-hdpi/ic_notify.png | Bin 0 -> 855 bytes app/src/main/res/drawable-hdpi/splash.png | Bin 0 -> 10238 bytes .../main/res/drawable-mdpi/elephant_error.png | Bin 0 -> 17853 bytes .../res/drawable-mdpi/elephant_friend.png | Bin 0 -> 19435 bytes .../drawable-mdpi/elephant_friend_empty.png | Bin 0 -> 23402 bytes .../res/drawable-mdpi/elephant_offline.png | Bin 0 -> 18198 bytes app/src/main/res/drawable-mdpi/ic_notify.png | Bin 0 -> 604 bytes app/src/main/res/drawable-mdpi/splash.png | Bin 0 -> 5949 bytes .../main/res/drawable-v24/ic_notoemoji.xml | 51 + .../drawable-v26/ic_launcher_foreground.xml | 30 + .../drawable-v26/launcher_shadow_gradient.xml | 12 + .../res/drawable-xhdpi/elephant_error.png | Bin 0 -> 47021 bytes .../res/drawable-xhdpi/elephant_friend.png | Bin 0 -> 48709 bytes .../drawable-xhdpi/elephant_friend_empty.png | Bin 0 -> 62276 bytes .../res/drawable-xhdpi/elephant_offline.png | Bin 0 -> 45673 bytes app/src/main/res/drawable-xhdpi/ic_notify.png | Bin 0 -> 1294 bytes app/src/main/res/drawable-xhdpi/splash.png | Bin 0 -> 14473 bytes .../res/drawable-xxhdpi/elephant_error.png | Bin 0 -> 84462 bytes .../res/drawable-xxhdpi/elephant_friend.png | Bin 0 -> 85936 bytes .../drawable-xxhdpi/elephant_friend_empty.png | Bin 0 -> 115492 bytes .../res/drawable-xxhdpi/elephant_offline.png | Bin 0 -> 77054 bytes .../main/res/drawable-xxhdpi/ic_notify.png | Bin 0 -> 1710 bytes app/src/main/res/drawable-xxhdpi/splash.png | Bin 0 -> 25007 bytes .../res/drawable-xxxhdpi/elephant_error.png | Bin 0 -> 130895 bytes .../res/drawable-xxxhdpi/elephant_friend.png | Bin 0 -> 124071 bytes .../elephant_friend_empty.png | Bin 0 -> 180584 bytes .../res/drawable-xxxhdpi/elephant_offline.png | Bin 0 -> 117637 bytes .../main/res/drawable-xxxhdpi/ic_notify.png | Bin 0 -> 2642 bytes app/src/main/res/drawable-xxxhdpi/splash.png | Bin 0 -> 37466 bytes app/src/main/res/drawable/avatar_border.xml | 6 + app/src/main/res/drawable/avatar_default.xml | 42 + .../drawable/background_dialog_activity.xml | 5 + .../main/res/drawable/background_splash.xml | 11 + app/src/main/res/drawable/card_frame.xml | 5 + .../res/drawable/card_image_placeholder.xml | 10 + .../res/drawable/conversation_thread_line.xml | 6 + .../res/drawable/description_bg_expanded.xml | 10 + app/src/main/res/drawable/ic_access_time.xml | 9 + .../main/res/drawable/ic_account_settings.xml | 13 + .../main/res/drawable/ic_add_a_photo_32dp.xml | 4 + app/src/main/res/drawable/ic_alert_circle.xml | 8 + .../main/res/drawable/ic_attach_file_24dp.xml | 9 + app/src/main/res/drawable/ic_bbcode_24dp.xml | 18 + app/src/main/res/drawable/ic_blobmoji.xml | 11 + .../main/res/drawable/ic_bookmark_24dp.xml | 9 + .../res/drawable/ic_bookmark_active_24dp.xml | 9 + app/src/main/res/drawable/ic_bot_24dp.xml | 8 + app/src/main/res/drawable/ic_briefcase.xml | 9 + .../main/res/drawable/ic_bullhorn_24dp.xml | 9 + app/src/main/res/drawable/ic_cancel_24dp.xml | 9 + app/src/main/res/drawable/ic_check_24dp.xml | 9 + app/src/main/res/drawable/ic_check_32dp.xml | 4 + .../ic_check_box_outline_blank_18dp.xml | 9 + app/src/main/res/drawable/ic_check_circle.xml | 8 + app/src/main/res/drawable/ic_clear_24dp.xml | 9 + app/src/main/res/drawable/ic_close_24dp.xml | 9 + app/src/main/res/drawable/ic_create_24dp.xml | 9 + app/src/main/res/drawable/ic_cw_24dp.xml | 9 + .../res/drawable/ic_drag_indicator_24dp.xml | 9 + .../drawable/ic_drag_indicator_horiz_24dp.xml | 9 + app/src/main/res/drawable/ic_email_24dp.xml | 7 + app/src/main/res/drawable/ic_emoji_24dp.xml | 9 + app/src/main/res/drawable/ic_emoji_34dp.xml | 9 + .../main/res/drawable/ic_exit_to_app_24px.xml | 9 + app/src/main/res/drawable/ic_eye_24dp.xml | 9 + .../main/res/drawable/ic_favourite_24dp.xml | 9 + .../res/drawable/ic_favourite_active_24dp.xml | 9 + .../drawable/ic_file_download_black_24dp.xml | 9 + app/src/main/res/drawable/ic_forum_24px.xml | 9 + app/src/main/res/drawable/ic_hashtag.xml | 7 + .../main/res/drawable/ic_hide_media_24dp.xml | 9 + app/src/main/res/drawable/ic_home_24dp.xml | 9 + app/src/main/res/drawable/ic_html_24dp.xml | 18 + app/src/main/res/drawable/ic_list.xml | 9 + app/src/main/res/drawable/ic_local_24dp.xml | 7 + .../main/res/drawable/ic_lock_open_24dp.xml | 9 + .../res/drawable/ic_lock_outline_24dp.xml | 9 + app/src/main/res/drawable/ic_logout.xml | 10 + app/src/main/res/drawable/ic_markdown.xml | 15 + .../main/res/drawable/ic_menu_share_24dp.xml | 25 + .../main/res/drawable/ic_more_horiz_24dp.xml | 9 + .../main/res/drawable/ic_music_box_24dp.xml | 8 + .../drawable/ic_music_box_preview_24dp.xml | 8 + app/src/main/res/drawable/ic_mute_24dp.xml | 12 + app/src/main/res/drawable/ic_notebook.xml | 8 + .../res/drawable/ic_notifications_24dp.xml | 9 + .../drawable/ic_notifications_active_24dp.xml | 13 + .../drawable/ic_notifications_off_24dp.xml | 12 + app/src/main/res/drawable/ic_notoemoji.xml | 23 + .../main/res/drawable/ic_person_add_24dp.xml | 9 + app/src/main/res/drawable/ic_photo_24dp.xml | 9 + .../main/res/drawable/ic_play_indicator.xml | 8 + app/src/main/res/drawable/ic_plus_24dp.xml | 9 + app/src/main/res/drawable/ic_poll_24dp.xml | 9 + app/src/main/res/drawable/ic_preview_24dp.xml | 9 + app/src/main/res/drawable/ic_public_24dp.xml | 9 + .../ic_radio_button_unchecked_18dp.xml | 9 + app/src/main/res/drawable/ic_reblog_18dp.xml | 9 + app/src/main/res/drawable/ic_reblog_24dp.xml | 9 + .../res/drawable/ic_reblog_active_24dp.xml | 9 + .../res/drawable/ic_reblog_direct_24dp.xml | 10 + .../res/drawable/ic_reblog_private_24dp.xml | 9 + .../ic_reblog_private_active_24dp.xml | 9 + app/src/main/res/drawable/ic_reject_24dp.xml | 9 + app/src/main/res/drawable/ic_repeat_24dp.xml | 9 + app/src/main/res/drawable/ic_reply_18dp.xml | 10 + app/src/main/res/drawable/ic_reply_24dp.xml | 10 + .../main/res/drawable/ic_reply_all_24dp.xml | 10 + app/src/main/res/drawable/ic_send_24dp.xml | 10 + app/src/main/res/drawable/ic_settings.xml | 10 + app/src/main/res/drawable/ic_star_24dp.xml | 9 + app/src/main/res/drawable/ic_sticker.xml | 16 + app/src/main/res/drawable/ic_tabs.xml | 8 + app/src/main/res/drawable/ic_tusky.xml | 11 + app/src/main/res/drawable/ic_twemoji.xml | 9 + app/src/main/res/drawable/ic_unmute_24dp.xml | 20 + .../main/res/drawable/ic_videocam_24dp.xml | 9 + .../drawable/materialdrawer_shape_large.xml | 5 + .../drawable/materialdrawer_shape_small.xml | 5 + app/src/main/res/drawable/md_bold.xml | 10 + app/src/main/res/drawable/md_code.xml | 10 + app/src/main/res/drawable/md_italic.xml | 10 + app/src/main/res/drawable/md_link.xml | 10 + .../main/res/drawable/md_strikethrough.xml | 10 + .../res/drawable/media_preview_outline.xml | 4 + .../main/res/drawable/media_warning_bg.xml | 5 + .../main/res/drawable/message_background.xml | 5 + .../res/drawable/poll_option_background.xml | 6 + .../main/res/drawable/poll_option_shape.xml | 7 + .../res/drawable/profile_badge_background.xml | 6 + .../drawable/report_success_background.xml | 4 + app/src/main/res/drawable/round_button.xml | 9 + app/src/main/res/drawable/spellcheck.xml | 8 + app/src/main/res/drawable/status_divider.xml | 6 + app/src/main/res/drawable/unread_shape.xml | 7 + .../res/layout-land/fragment_report_done.xml | 109 ++ .../res/layout-sw640dp/fragment_timeline.xml | 63 + .../fragment_timeline_notifications.xml | 85 + .../layout-sw640dp/fragment_view_thread.xml | 23 + app/src/main/res/layout/activity_about.xml | 126 ++ app/src/main/res/layout/activity_account.xml | 469 +++++ .../main/res/layout/activity_account_list.xml | 18 + .../res/layout/activity_announcements.xml | 41 + app/src/main/res/layout/activity_chat.xml | 254 +++ app/src/main/res/layout/activity_compose.xml | 483 +++++ app/src/main/res/layout/activity_drafts.xml | 34 + .../main/res/layout/activity_edit_profile.xml | 196 ++ app/src/main/res/layout/activity_filters.xml | 41 + app/src/main/res/layout/activity_license.xml | 261 +++ app/src/main/res/layout/activity_lists.xml | 58 + app/src/main/res/layout/activity_login.xml | 159 ++ app/src/main/res/layout/activity_main.xml | 87 + .../res/layout/activity_modal_timeline.xml | 20 + .../main/res/layout/activity_preferences.xml | 18 + app/src/main/res/layout/activity_report.xml | 21 + .../main/res/layout/activity_saved_toot.xml | 34 + .../res/layout/activity_scheduled_toot.xml | 53 + app/src/main/res/layout/activity_search.xml | 43 + .../main/res/layout/activity_statuslist.xml | 20 + .../res/layout/activity_tab_preference.xml | 81 + .../main/res/layout/activity_view_media.xml | 30 + app/src/main/res/layout/activity_view_tag.xml | 20 + .../main/res/layout/activity_view_thread.xml | 20 + app/src/main/res/layout/card_license.xml | 35 + app/src/main/res/layout/dialog_add_poll.xml | 57 + .../main/res/layout/dialog_emoji_keyboard.xml | 8 + .../main/res/layout/dialog_emojicompat.xml | 36 + app/src/main/res/layout/dialog_filter.xml | 37 + .../main/res/layout/dialog_mute_account.xml | 38 + .../main/res/layout/fragment_account_list.xml | 19 + .../res/layout/fragment_accounts_in_list.xml | 56 + .../res/layout/fragment_instance_list.xml | 25 + .../main/res/layout/fragment_report_done.xml | 108 ++ .../main/res/layout/fragment_report_note.xml | 129 ++ .../res/layout/fragment_report_statuses.xml | 72 + app/src/main/res/layout/fragment_search.xml | 49 + app/src/main/res/layout/fragment_timeline.xml | 53 + .../fragment_timeline_notifications.xml | 80 + .../main/res/layout/fragment_view_image.xml | 80 + .../main/res/layout/fragment_view_thread.xml | 17 + .../main/res/layout/fragment_view_video.xml | 44 + app/src/main/res/layout/item_account.xml | 67 + .../main/res/layout/item_account_field.xml | 39 + .../main/res/layout/item_add_poll_option.xml | 32 + app/src/main/res/layout/item_announcement.xml | 41 + .../res/layout/item_autocomplete_account.xml | 48 + .../res/layout/item_autocomplete_divider.xml | 5 + .../res/layout/item_autocomplete_emoji.xml | 33 + .../res/layout/item_autocomplete_hashtag.xml | 11 + app/src/main/res/layout/item_blocked_user.xml | 64 + app/src/main/res/layout/item_chat.xml | 125 ++ app/src/main/res/layout/item_conversation.xml | 314 ++++ app/src/main/res/layout/item_draft.xml | 95 + app/src/main/res/layout/item_edit_field.xml | 39 + app/src/main/res/layout/item_emoji_button.xml | 10 + .../res/layout/item_emoji_keyboard_emoji.xml | 20 + .../res/layout/item_emoji_keyboard_page.xml | 8 + .../layout/item_emoji_keyboard_sticker.xml | 12 + app/src/main/res/layout/item_emoji_picker.xml | 24 + app/src/main/res/layout/item_emoji_pref.xml | 115 ++ .../main/res/layout/item_emoji_reaction.xml | 17 + app/src/main/res/layout/item_follow.xml | 74 + .../main/res/layout/item_follow_request.xml | 77 + .../item_follow_request_notification.xml | 96 + app/src/main/res/layout/item_footer.xml | 12 + app/src/main/res/layout/item_hashtag.xml | 8 + app/src/main/res/layout/item_list.xml | 33 + .../main/res/layout/item_media_preview.xml | 190 ++ app/src/main/res/layout/item_muted_domain.xml | 41 + app/src/main/res/layout/item_muted_user.xml | 77 + .../main/res/layout/item_network_state.xml | 23 + app/src/main/res/layout/item_our_message.xml | 77 + app/src/main/res/layout/item_picker_list.xml | 9 + app/src/main/res/layout/item_poll.xml | 43 + .../res/layout/item_poll_preview_option.xml | 12 + .../main/res/layout/item_report_status.xml | 362 ++++ app/src/main/res/layout/item_saved_toot.xml | 28 + .../main/res/layout/item_scheduled_toot.xml | 40 + app/src/main/res/layout/item_status.xml | 445 +++++ .../res/layout/item_status_bottom_sheet.xml | 20 + .../main/res/layout/item_status_detailed.xml | 453 +++++ app/src/main/res/layout/item_status_muted.xml | 76 + .../res/layout/item_status_notification.xml | 178 ++ .../res/layout/item_status_placeholder.xml | 22 + .../main/res/layout/item_tab_preference.xml | 76 + .../res/layout/item_tab_preference_small.xml | 19 + .../main/res/layout/item_their_message.xml | 77 + .../res/layout/material_drawer_header.xml | 201 ++ .../main/res/layout/notifications_filter.xml | 17 + app/src/main/res/layout/search_view.xml | 8 + app/src/main/res/layout/toolbar_basic.xml | 16 + .../main/res/layout/view_account_moved.xml | 61 + .../res/layout/view_background_message.xml | 39 + .../main/res/layout/view_compose_options.xml | 62 + .../main/res/layout/view_compose_schedule.xml | 50 + app/src/main/res/layout/view_poll_preview.xml | 37 + app/src/main/res/menu/account_toolbar.xml | 39 + app/src/main/res/menu/chat_more.xml | 7 + app/src/main/res/menu/drafts.xml | 10 + .../main/res/menu/edit_profile_toolbar.xml | 9 + app/src/main/res/menu/emoji_reaction_more.xml | 13 + app/src/main/res/menu/list_actions.xml | 13 + app/src/main/res/menu/search_toolbar.xml | 11 + app/src/main/res/menu/status_more.xml | 44 + .../main/res/menu/status_more_for_user.xml | 43 + app/src/main/res/menu/view_media_toolbar.xml | 27 + app/src/main/res/menu/view_thread_toolbar.xml | 13 + app/src/main/res/raw/apache.txt | 51 + app/src/main/res/values-ar/strings.xml | 505 +++++ app/src/main/res/values-ber/strings.xml | 43 + app/src/main/res/values-bn-rBD/strings.xml | 427 +++++ app/src/main/res/values-bn-rIN/strings.xml | 473 +++++ app/src/main/res/values-ca/strings.xml | 458 +++++ app/src/main/res/values-ckb/strings.xml | 479 +++++ app/src/main/res/values-cs/strings.xml | 481 +++++ app/src/main/res/values-cy/strings.xml | 303 +++ app/src/main/res/values-de/strings.xml | 473 +++++ app/src/main/res/values-en-rAU/strings.xml | 2 + app/src/main/res/values-en-rGB/strings.xml | 23 + app/src/main/res/values-eo/strings.xml | 485 +++++ app/src/main/res/values-es/strings.xml | 511 +++++ app/src/main/res/values-eu/strings.xml | 472 +++++ app/src/main/res/values-fa/strings.xml | 477 +++++ app/src/main/res/values-fr/strings.xml | 508 +++++ app/src/main/res/values-ga/strings.xml | 489 +++++ app/src/main/res/values-gd/strings.xml | 55 + app/src/main/res/values-hi/strings.xml | 408 ++++ app/src/main/res/values-hu/strings.xml | 479 +++++ app/src/main/res/values-is/strings.xml | 470 +++++ app/src/main/res/values-it/strings.xml | 496 +++++ app/src/main/res/values-ja/strings.xml | 459 +++++ app/src/main/res/values-kab/strings.xml | 277 +++ app/src/main/res/values-ko/strings.xml | 426 +++++ app/src/main/res/values-large/dimens.xml | 6 + app/src/main/res/values-large/styles.xml | 26 + app/src/main/res/values-ml/strings.xml | 114 ++ .../main/res/values-night/theme_colors.xml | 26 + app/src/main/res/values-nl/strings.xml | 456 +++++ app/src/main/res/values-no-rNB/strings.xml | 499 +++++ app/src/main/res/values-oc/strings.xml | 456 +++++ app/src/main/res/values-pa/strings.xml | 2 + app/src/main/res/values-pl/strings.xml | 484 +++++ app/src/main/res/values-pt-rBR/strings.xml | 475 +++++ app/src/main/res/values-ru/strings.xml | 508 +++++ app/src/main/res/values-sa/strings.xml | 465 +++++ app/src/main/res/values-sk/strings.xml | 135 ++ app/src/main/res/values-sl/strings.xml | 445 +++++ app/src/main/res/values-small/integer.xml | 4 + app/src/main/res/values-sv/strings.xml | 503 +++++ .../main/res/values-sw380dp/toot_button.xml | 7 + app/src/main/res/values-ta/strings.xml | 282 +++ app/src/main/res/values-te/strings.xml | 2 + app/src/main/res/values-th/strings.xml | 458 +++++ app/src/main/res/values-tr/strings.xml | 473 +++++ app/src/main/res/values-uk/strings.xml | 150 ++ app/src/main/res/values-v27/styles.xml | 26 + app/src/main/res/values-vi/strings.xml | 491 +++++ app/src/main/res/values-w640dp/dimens.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 484 +++++ app/src/main/res/values-zh-rHK/strings.xml | 405 ++++ app/src/main/res/values-zh-rMO/strings.xml | 403 ++++ app/src/main/res/values-zh-rSG/strings.xml | 407 ++++ app/src/main/res/values-zh-rTW/strings.xml | 431 +++++ app/src/main/res/values/actions.xml | 26 + app/src/main/res/values/attrs.xml | 27 + app/src/main/res/values/colors.xml | 46 + app/src/main/res/values/dimens.xml | 68 + app/src/main/res/values/donottranslate.xml | 190 ++ app/src/main/res/values/ids.xml | 4 + app/src/main/res/values/integers.xml | 4 + app/src/main/res/values/string-arrays.xml | 26 + app/src/main/res/values/strings.xml | 613 ++++++ app/src/main/res/values/styles.xml | 190 ++ app/src/main/res/values/theme_colors.xml | 26 + app/src/main/res/values/toot_button.xml | 7 + app/src/main/res/xml/file_paths.xml | 5 + app/src/main/res/xml/searchable.xml | 5 + app/src/main/res/xml/share_shortcuts.xml | 8 + app/src/main/splash.svg | 173 ++ .../java/android/text/FakeSpannableString.kt | 50 + .../tusky/BottomSheetActivityTest.kt | 323 ++++ .../tusky/ComposeActivityTest.kt | 558 ++++++ .../tusky/ComposeTokenizerTest.kt | 94 + .../com/keylesspalace/tusky/FilterTest.kt | 256 +++ .../keylesspalace/tusky/FocalPointUtilTest.kt | 110 ++ .../com/keylesspalace/tusky/SpanUtilsTest.kt | 178 ++ .../keylesspalace/tusky/StringUtilsTest.kt | 46 + .../keylesspalace/tusky/TuskyApplication.kt | 65 + .../tusky/fragment/TimelineRepositoryTest.kt | 340 ++++ .../tusky/util/EmojiCompatFontTest.kt | 47 + .../tusky/util/SmartLengthInputFilterTest.kt | 80 + .../tusky/util/VersionUtilsTest.kt | 36 + .../tusky/util/ViewPager2FixTest.java | 34 + assets/avatar_default.svg | 36 + assets/fdroid_badge.png | Bin 0 -> 15174 bytes assets/splash.xcf | Bin 0 -> 43720 bytes assets/tusky_logo_borderless.png | Bin 0 -> 36521 bytes build.gradle | 28 + debug.keystore | Bin 0 -> 2235 bytes .../metadata/android/en-US/changelogs/167.txt | 12 + .../metadata/android/en-US/changelogs/168.txt | 5 + .../android/en-US/full_description.txt | 6 + .../android/en-US/images/featureGraphic.png | Bin 0 -> 51784 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 12200 bytes .../images/phoneScreenshots/00_login.png | Bin 0 -> 233052 bytes .../images/phoneScreenshots/01_timeline.png | Bin 0 -> 925293 bytes .../images/phoneScreenshots/02_compose.png | Bin 0 -> 281987 bytes .../images/phoneScreenshots/03_profile.png | Bin 0 -> 294178 bytes .../images/phoneScreenshots/04_emojis.png | Bin 0 -> 203440 bytes .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + .../metadata/android/ru/changelogs/167.txt | 12 + .../metadata/android/ru/changelogs/168.txt | 5 + .../metadata/android/ru/full_description.txt | 6 + .../metadata/android/ru/short_description.txt | 1 + fastlane/metadata/android/ru/title.txt | 1 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++ gradlew.bat | 101 + instance-build.gradle | 19 + scripts/import_translations.sh | 47 + scripts/xq.py | 65 + settings.gradle | 1 + 814 files changed, 96885 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json create mode 100644 app/src/androidTest/java/com/keylesspalace/tusky/ExampleInstrumentedTest.java create mode 100644 app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt create mode 100644 app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt create mode 100644 app/src/blue/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/blue/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/blue/res/mipmap-hdpi/ic_shortcut_compose.png create mode 100644 app/src/blue/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/blue/res/mipmap-mdpi/ic_shortcut_compose.png create mode 100644 app/src/blue/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/blue/res/mipmap-xhdpi/ic_shortcut_compose.png create mode 100644 app/src/blue/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/blue/res/mipmap-xxhdpi/ic_shortcut_compose.png create mode 100644 app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/blue/res/mipmap-xxxhdpi/ic_shortcut_compose.png create mode 100644 app/src/green/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/green/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/green/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/green/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/green/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/green/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/husky/res/values-ar/husky_generated.xml create mode 100644 app/src/husky/res/values-ar/strings.xml create mode 100644 app/src/husky/res/values-ber/husky_generated.xml create mode 100644 app/src/husky/res/values-bn-rBD/husky_generated.xml create mode 100644 app/src/husky/res/values-bn-rIN/husky_generated.xml create mode 100644 app/src/husky/res/values-ca/husky_generated.xml create mode 100644 app/src/husky/res/values-ca/strings.xml create mode 100644 app/src/husky/res/values-ckb/husky_generated.xml create mode 100644 app/src/husky/res/values-cs/husky_generated.xml create mode 100644 app/src/husky/res/values-cy/husky_generated.xml create mode 100644 app/src/husky/res/values-de/husky_generated.xml create mode 100644 app/src/husky/res/values-de/strings.xml create mode 100644 app/src/husky/res/values-en-rAU/husky_generated.xml create mode 100644 app/src/husky/res/values-en-rGB/husky_generated.xml create mode 100644 app/src/husky/res/values-en-rGB/strings.xml create mode 100644 app/src/husky/res/values-eo/husky_generated.xml create mode 100644 app/src/husky/res/values-es/husky_generated.xml create mode 100644 app/src/husky/res/values-es/strings.xml create mode 100644 app/src/husky/res/values-eu/husky_generated.xml create mode 100644 app/src/husky/res/values-fa/husky_generated.xml create mode 100644 app/src/husky/res/values-fr/husky_generated.xml create mode 100644 app/src/husky/res/values-fr/strings.xml create mode 100644 app/src/husky/res/values-ga/husky_generated.xml create mode 100644 app/src/husky/res/values-gd/husky_generated.xml create mode 100644 app/src/husky/res/values-hi/husky_generated.xml create mode 100644 app/src/husky/res/values-hi/strings.xml create mode 100644 app/src/husky/res/values-hu/husky_generated.xml create mode 100644 app/src/husky/res/values-is/husky_generated.xml create mode 100644 app/src/husky/res/values-it/husky_generated.xml create mode 100644 app/src/husky/res/values-ja/husky_generated.xml create mode 100644 app/src/husky/res/values-ja/strings.xml create mode 100644 app/src/husky/res/values-kab/husky_generated.xml create mode 100644 app/src/husky/res/values-ko/husky_generated.xml create mode 100644 app/src/husky/res/values-ml/husky_generated.xml create mode 100644 app/src/husky/res/values-nb-rNO/strings.xml create mode 100644 app/src/husky/res/values-nl/husky_generated.xml create mode 100644 app/src/husky/res/values-nn/strings.xml create mode 100644 app/src/husky/res/values-no-rNB/husky_generated.xml create mode 100644 app/src/husky/res/values-oc/husky_generated.xml create mode 100644 app/src/husky/res/values-pa/husky_generated.xml create mode 100644 app/src/husky/res/values-pl/husky_generated.xml create mode 100644 app/src/husky/res/values-pl/strings.xml create mode 100644 app/src/husky/res/values-pt-rBR/husky_generated.xml create mode 100644 app/src/husky/res/values-pt-rBR/strings.xml create mode 100644 app/src/husky/res/values-pt-rPT/strings.xml create mode 100644 app/src/husky/res/values-ru/husky_generated.xml create mode 100644 app/src/husky/res/values-ru/strings.xml create mode 100644 app/src/husky/res/values-sa/husky_generated.xml create mode 100644 app/src/husky/res/values-sk/husky_generated.xml create mode 100644 app/src/husky/res/values-sl/husky_generated.xml create mode 100644 app/src/husky/res/values-sv/husky_generated.xml create mode 100644 app/src/husky/res/values-sv/strings.xml create mode 100644 app/src/husky/res/values-ta/husky_generated.xml create mode 100644 app/src/husky/res/values-te/husky_generated.xml create mode 100644 app/src/husky/res/values-th/husky_generated.xml create mode 100644 app/src/husky/res/values-th/strings.xml create mode 100644 app/src/husky/res/values-tr/husky_generated.xml create mode 100644 app/src/husky/res/values-tr/strings.xml create mode 100644 app/src/husky/res/values-uk/husky_generated.xml create mode 100644 app/src/husky/res/values-uk/strings.xml create mode 100644 app/src/husky/res/values-vi/husky_generated.xml create mode 100644 app/src/husky/res/values-zh-rCN/husky_generated.xml create mode 100644 app/src/husky/res/values-zh-rHK/husky_generated.xml create mode 100644 app/src/husky/res/values-zh-rMO/husky_generated.xml create mode 100644 app/src/husky/res/values-zh-rSG/husky_generated.xml create mode 100644 app/src/husky/res/values-zh-rTW/husky_generated.xml create mode 100644 app/src/husky/res/values/donottranslate.xml create mode 100644 app/src/husky/res/values/husky_donottranslate.xml create mode 100644 app/src/husky/res/values/husky_generated.xml create mode 100644 app/src/husky/res/values/strings.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_bbcode.svg create mode 100644 app/src/main/ic_html.svg create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/ic_launcher.svg create mode 100644 app/src/main/ic_launcher_.svg create mode 100644 app/src/main/ic_launcher_foreground.svg create mode 100644 app/src/main/ic_sticker.svg create mode 100644 app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/BaseActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/MainActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/TabData.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/StickerAdapater.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/chat/ChatViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/common/CommonComposeViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/common/MediaUploader.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/ChatEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/ChatMessageEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/Converters.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TootDao.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/GlideModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Account.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Card.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Chat.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/NodeInfo.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Status.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Sticker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/StreamEvent.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/service/StreamingService.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Either.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Emojis.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Listing.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/OmittedDomainFetcher.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/PairedList.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Resource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/CustomEmojiTextView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt create mode 100644 app/src/main/res/anim/explode.xml create mode 100644 app/src/main/res/anim/fade_in.xml create mode 100644 app/src/main/res/anim/fade_out.xml create mode 100644 app/src/main/res/anim/slide_from_left.xml create mode 100644 app/src/main/res/anim/slide_from_right.xml create mode 100644 app/src/main/res/anim/slide_to_left.xml create mode 100644 app/src/main/res/anim/slide_to_right.xml create mode 100644 app/src/main/res/color/account_tab_font_color.xml create mode 100644 app/src/main/res/color/color_background_transparent_60.xml create mode 100644 app/src/main/res/color/compound_button_color.xml create mode 100644 app/src/main/res/color/emoji_reaction_button.xml create mode 100644 app/src/main/res/color/text_input_layout_box_stroke_color.xml create mode 100644 app/src/main/res/drawable-hdpi/elephant_error.png create mode 100644 app/src/main/res/drawable-hdpi/elephant_friend.png create mode 100644 app/src/main/res/drawable-hdpi/elephant_friend_empty.png create mode 100644 app/src/main/res/drawable-hdpi/elephant_offline.png create mode 100644 app/src/main/res/drawable-hdpi/ic_notify.png create mode 100644 app/src/main/res/drawable-hdpi/splash.png create mode 100644 app/src/main/res/drawable-mdpi/elephant_error.png create mode 100644 app/src/main/res/drawable-mdpi/elephant_friend.png create mode 100644 app/src/main/res/drawable-mdpi/elephant_friend_empty.png create mode 100644 app/src/main/res/drawable-mdpi/elephant_offline.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notify.png create mode 100644 app/src/main/res/drawable-mdpi/splash.png create mode 100644 app/src/main/res/drawable-v24/ic_notoemoji.xml create mode 100644 app/src/main/res/drawable-v26/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable-v26/launcher_shadow_gradient.xml create mode 100644 app/src/main/res/drawable-xhdpi/elephant_error.png create mode 100644 app/src/main/res/drawable-xhdpi/elephant_friend.png create mode 100644 app/src/main/res/drawable-xhdpi/elephant_friend_empty.png create mode 100644 app/src/main/res/drawable-xhdpi/elephant_offline.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notify.png create mode 100644 app/src/main/res/drawable-xhdpi/splash.png create mode 100644 app/src/main/res/drawable-xxhdpi/elephant_error.png create mode 100644 app/src/main/res/drawable-xxhdpi/elephant_friend.png create mode 100644 app/src/main/res/drawable-xxhdpi/elephant_friend_empty.png create mode 100644 app/src/main/res/drawable-xxhdpi/elephant_offline.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notify.png create mode 100644 app/src/main/res/drawable-xxhdpi/splash.png create mode 100644 app/src/main/res/drawable-xxxhdpi/elephant_error.png create mode 100644 app/src/main/res/drawable-xxxhdpi/elephant_friend.png create mode 100644 app/src/main/res/drawable-xxxhdpi/elephant_friend_empty.png create mode 100644 app/src/main/res/drawable-xxxhdpi/elephant_offline.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_notify.png create mode 100644 app/src/main/res/drawable-xxxhdpi/splash.png create mode 100644 app/src/main/res/drawable/avatar_border.xml create mode 100644 app/src/main/res/drawable/avatar_default.xml create mode 100644 app/src/main/res/drawable/background_dialog_activity.xml create mode 100644 app/src/main/res/drawable/background_splash.xml create mode 100644 app/src/main/res/drawable/card_frame.xml create mode 100644 app/src/main/res/drawable/card_image_placeholder.xml create mode 100644 app/src/main/res/drawable/conversation_thread_line.xml create mode 100644 app/src/main/res/drawable/description_bg_expanded.xml create mode 100644 app/src/main/res/drawable/ic_access_time.xml create mode 100644 app/src/main/res/drawable/ic_account_settings.xml create mode 100644 app/src/main/res/drawable/ic_add_a_photo_32dp.xml create mode 100644 app/src/main/res/drawable/ic_alert_circle.xml create mode 100644 app/src/main/res/drawable/ic_attach_file_24dp.xml create mode 100644 app/src/main/res/drawable/ic_bbcode_24dp.xml create mode 100644 app/src/main/res/drawable/ic_blobmoji.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_24dp.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_active_24dp.xml create mode 100644 app/src/main/res/drawable/ic_bot_24dp.xml create mode 100644 app/src/main/res/drawable/ic_briefcase.xml create mode 100644 app/src/main/res/drawable/ic_bullhorn_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cancel_24dp.xml create mode 100644 app/src/main/res/drawable/ic_check_24dp.xml create mode 100644 app/src/main/res/drawable/ic_check_32dp.xml create mode 100644 app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/drawable/ic_clear_24dp.xml create mode 100644 app/src/main/res/drawable/ic_close_24dp.xml create mode 100644 app/src/main/res/drawable/ic_create_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cw_24dp.xml create mode 100644 app/src/main/res/drawable/ic_drag_indicator_24dp.xml create mode 100644 app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml create mode 100644 app/src/main/res/drawable/ic_email_24dp.xml create mode 100644 app/src/main/res/drawable/ic_emoji_24dp.xml create mode 100644 app/src/main/res/drawable/ic_emoji_34dp.xml create mode 100644 app/src/main/res/drawable/ic_exit_to_app_24px.xml create mode 100644 app/src/main/res/drawable/ic_eye_24dp.xml create mode 100644 app/src/main/res/drawable/ic_favourite_24dp.xml create mode 100644 app/src/main/res/drawable/ic_favourite_active_24dp.xml create mode 100644 app/src/main/res/drawable/ic_file_download_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_forum_24px.xml create mode 100644 app/src/main/res/drawable/ic_hashtag.xml create mode 100644 app/src/main/res/drawable/ic_hide_media_24dp.xml create mode 100644 app/src/main/res/drawable/ic_home_24dp.xml create mode 100644 app/src/main/res/drawable/ic_html_24dp.xml create mode 100644 app/src/main/res/drawable/ic_list.xml create mode 100644 app/src/main/res/drawable/ic_local_24dp.xml create mode 100644 app/src/main/res/drawable/ic_lock_open_24dp.xml create mode 100644 app/src/main/res/drawable/ic_lock_outline_24dp.xml create mode 100644 app/src/main/res/drawable/ic_logout.xml create mode 100644 app/src/main/res/drawable/ic_markdown.xml create mode 100644 app/src/main/res/drawable/ic_menu_share_24dp.xml create mode 100644 app/src/main/res/drawable/ic_more_horiz_24dp.xml create mode 100644 app/src/main/res/drawable/ic_music_box_24dp.xml create mode 100644 app/src/main/res/drawable/ic_music_box_preview_24dp.xml create mode 100644 app/src/main/res/drawable/ic_mute_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notebook.xml create mode 100644 app/src/main/res/drawable/ic_notifications_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notifications_active_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notifications_off_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notoemoji.xml create mode 100644 app/src/main/res/drawable/ic_person_add_24dp.xml create mode 100644 app/src/main/res/drawable/ic_photo_24dp.xml create mode 100644 app/src/main/res/drawable/ic_play_indicator.xml create mode 100644 app/src/main/res/drawable/ic_plus_24dp.xml create mode 100644 app/src/main/res/drawable/ic_poll_24dp.xml create mode 100644 app/src/main/res/drawable/ic_preview_24dp.xml create mode 100644 app/src/main/res/drawable/ic_public_24dp.xml create mode 100644 app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_18dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_active_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_direct_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_private_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reblog_private_active_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reject_24dp.xml create mode 100644 app/src/main/res/drawable/ic_repeat_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reply_18dp.xml create mode 100644 app/src/main/res/drawable/ic_reply_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reply_all_24dp.xml create mode 100644 app/src/main/res/drawable/ic_send_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_star_24dp.xml create mode 100644 app/src/main/res/drawable/ic_sticker.xml create mode 100644 app/src/main/res/drawable/ic_tabs.xml create mode 100644 app/src/main/res/drawable/ic_tusky.xml create mode 100644 app/src/main/res/drawable/ic_twemoji.xml create mode 100644 app/src/main/res/drawable/ic_unmute_24dp.xml create mode 100644 app/src/main/res/drawable/ic_videocam_24dp.xml create mode 100644 app/src/main/res/drawable/materialdrawer_shape_large.xml create mode 100644 app/src/main/res/drawable/materialdrawer_shape_small.xml create mode 100644 app/src/main/res/drawable/md_bold.xml create mode 100644 app/src/main/res/drawable/md_code.xml create mode 100644 app/src/main/res/drawable/md_italic.xml create mode 100644 app/src/main/res/drawable/md_link.xml create mode 100644 app/src/main/res/drawable/md_strikethrough.xml create mode 100644 app/src/main/res/drawable/media_preview_outline.xml create mode 100644 app/src/main/res/drawable/media_warning_bg.xml create mode 100644 app/src/main/res/drawable/message_background.xml create mode 100644 app/src/main/res/drawable/poll_option_background.xml create mode 100644 app/src/main/res/drawable/poll_option_shape.xml create mode 100644 app/src/main/res/drawable/profile_badge_background.xml create mode 100644 app/src/main/res/drawable/report_success_background.xml create mode 100644 app/src/main/res/drawable/round_button.xml create mode 100644 app/src/main/res/drawable/spellcheck.xml create mode 100644 app/src/main/res/drawable/status_divider.xml create mode 100644 app/src/main/res/drawable/unread_shape.xml create mode 100644 app/src/main/res/layout-land/fragment_report_done.xml create mode 100644 app/src/main/res/layout-sw640dp/fragment_timeline.xml create mode 100644 app/src/main/res/layout-sw640dp/fragment_timeline_notifications.xml create mode 100644 app/src/main/res/layout-sw640dp/fragment_view_thread.xml create mode 100644 app/src/main/res/layout/activity_about.xml create mode 100644 app/src/main/res/layout/activity_account.xml create mode 100644 app/src/main/res/layout/activity_account_list.xml create mode 100644 app/src/main/res/layout/activity_announcements.xml create mode 100644 app/src/main/res/layout/activity_chat.xml create mode 100644 app/src/main/res/layout/activity_compose.xml create mode 100644 app/src/main/res/layout/activity_drafts.xml create mode 100644 app/src/main/res/layout/activity_edit_profile.xml create mode 100644 app/src/main/res/layout/activity_filters.xml create mode 100644 app/src/main/res/layout/activity_license.xml create mode 100644 app/src/main/res/layout/activity_lists.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_modal_timeline.xml create mode 100644 app/src/main/res/layout/activity_preferences.xml create mode 100644 app/src/main/res/layout/activity_report.xml create mode 100644 app/src/main/res/layout/activity_saved_toot.xml create mode 100644 app/src/main/res/layout/activity_scheduled_toot.xml create mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/activity_statuslist.xml create mode 100644 app/src/main/res/layout/activity_tab_preference.xml create mode 100644 app/src/main/res/layout/activity_view_media.xml create mode 100644 app/src/main/res/layout/activity_view_tag.xml create mode 100644 app/src/main/res/layout/activity_view_thread.xml create mode 100644 app/src/main/res/layout/card_license.xml create mode 100644 app/src/main/res/layout/dialog_add_poll.xml create mode 100644 app/src/main/res/layout/dialog_emoji_keyboard.xml create mode 100644 app/src/main/res/layout/dialog_emojicompat.xml create mode 100644 app/src/main/res/layout/dialog_filter.xml create mode 100644 app/src/main/res/layout/dialog_mute_account.xml create mode 100644 app/src/main/res/layout/fragment_account_list.xml create mode 100644 app/src/main/res/layout/fragment_accounts_in_list.xml create mode 100644 app/src/main/res/layout/fragment_instance_list.xml create mode 100644 app/src/main/res/layout/fragment_report_done.xml create mode 100644 app/src/main/res/layout/fragment_report_note.xml create mode 100644 app/src/main/res/layout/fragment_report_statuses.xml create mode 100644 app/src/main/res/layout/fragment_search.xml create mode 100644 app/src/main/res/layout/fragment_timeline.xml create mode 100644 app/src/main/res/layout/fragment_timeline_notifications.xml create mode 100644 app/src/main/res/layout/fragment_view_image.xml create mode 100644 app/src/main/res/layout/fragment_view_thread.xml create mode 100644 app/src/main/res/layout/fragment_view_video.xml create mode 100644 app/src/main/res/layout/item_account.xml create mode 100644 app/src/main/res/layout/item_account_field.xml create mode 100644 app/src/main/res/layout/item_add_poll_option.xml create mode 100644 app/src/main/res/layout/item_announcement.xml create mode 100644 app/src/main/res/layout/item_autocomplete_account.xml create mode 100644 app/src/main/res/layout/item_autocomplete_divider.xml create mode 100644 app/src/main/res/layout/item_autocomplete_emoji.xml create mode 100644 app/src/main/res/layout/item_autocomplete_hashtag.xml create mode 100644 app/src/main/res/layout/item_blocked_user.xml create mode 100644 app/src/main/res/layout/item_chat.xml create mode 100644 app/src/main/res/layout/item_conversation.xml create mode 100644 app/src/main/res/layout/item_draft.xml create mode 100644 app/src/main/res/layout/item_edit_field.xml create mode 100644 app/src/main/res/layout/item_emoji_button.xml create mode 100644 app/src/main/res/layout/item_emoji_keyboard_emoji.xml create mode 100644 app/src/main/res/layout/item_emoji_keyboard_page.xml create mode 100644 app/src/main/res/layout/item_emoji_keyboard_sticker.xml create mode 100644 app/src/main/res/layout/item_emoji_picker.xml create mode 100644 app/src/main/res/layout/item_emoji_pref.xml create mode 100644 app/src/main/res/layout/item_emoji_reaction.xml create mode 100644 app/src/main/res/layout/item_follow.xml create mode 100644 app/src/main/res/layout/item_follow_request.xml create mode 100644 app/src/main/res/layout/item_follow_request_notification.xml create mode 100644 app/src/main/res/layout/item_footer.xml create mode 100644 app/src/main/res/layout/item_hashtag.xml create mode 100644 app/src/main/res/layout/item_list.xml create mode 100644 app/src/main/res/layout/item_media_preview.xml create mode 100644 app/src/main/res/layout/item_muted_domain.xml create mode 100644 app/src/main/res/layout/item_muted_user.xml create mode 100644 app/src/main/res/layout/item_network_state.xml create mode 100644 app/src/main/res/layout/item_our_message.xml create mode 100644 app/src/main/res/layout/item_picker_list.xml create mode 100644 app/src/main/res/layout/item_poll.xml create mode 100644 app/src/main/res/layout/item_poll_preview_option.xml create mode 100644 app/src/main/res/layout/item_report_status.xml create mode 100644 app/src/main/res/layout/item_saved_toot.xml create mode 100644 app/src/main/res/layout/item_scheduled_toot.xml create mode 100644 app/src/main/res/layout/item_status.xml create mode 100644 app/src/main/res/layout/item_status_bottom_sheet.xml create mode 100644 app/src/main/res/layout/item_status_detailed.xml create mode 100644 app/src/main/res/layout/item_status_muted.xml create mode 100644 app/src/main/res/layout/item_status_notification.xml create mode 100644 app/src/main/res/layout/item_status_placeholder.xml create mode 100644 app/src/main/res/layout/item_tab_preference.xml create mode 100644 app/src/main/res/layout/item_tab_preference_small.xml create mode 100644 app/src/main/res/layout/item_their_message.xml create mode 100644 app/src/main/res/layout/material_drawer_header.xml create mode 100644 app/src/main/res/layout/notifications_filter.xml create mode 100644 app/src/main/res/layout/search_view.xml create mode 100644 app/src/main/res/layout/toolbar_basic.xml create mode 100644 app/src/main/res/layout/view_account_moved.xml create mode 100644 app/src/main/res/layout/view_background_message.xml create mode 100644 app/src/main/res/layout/view_compose_options.xml create mode 100644 app/src/main/res/layout/view_compose_schedule.xml create mode 100644 app/src/main/res/layout/view_poll_preview.xml create mode 100644 app/src/main/res/menu/account_toolbar.xml create mode 100644 app/src/main/res/menu/chat_more.xml create mode 100644 app/src/main/res/menu/drafts.xml create mode 100644 app/src/main/res/menu/edit_profile_toolbar.xml create mode 100644 app/src/main/res/menu/emoji_reaction_more.xml create mode 100644 app/src/main/res/menu/list_actions.xml create mode 100644 app/src/main/res/menu/search_toolbar.xml create mode 100644 app/src/main/res/menu/status_more.xml create mode 100644 app/src/main/res/menu/status_more_for_user.xml create mode 100644 app/src/main/res/menu/view_media_toolbar.xml create mode 100644 app/src/main/res/menu/view_thread_toolbar.xml create mode 100644 app/src/main/res/raw/apache.txt create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-ber/strings.xml create mode 100644 app/src/main/res/values-bn-rBD/strings.xml create mode 100644 app/src/main/res/values-bn-rIN/strings.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-ckb/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-cy/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-en-rAU/strings.xml create mode 100644 app/src/main/res/values-en-rGB/strings.xml create mode 100644 app/src/main/res/values-eo/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-ga/strings.xml create mode 100644 app/src/main/res/values-gd/strings.xml create mode 100644 app/src/main/res/values-hi/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-is/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-kab/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-large/dimens.xml create mode 100644 app/src/main/res/values-large/styles.xml create mode 100644 app/src/main/res/values-ml/strings.xml create mode 100644 app/src/main/res/values-night/theme_colors.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-no-rNB/strings.xml create mode 100644 app/src/main/res/values-oc/strings.xml create mode 100644 app/src/main/res/values-pa/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sa/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-sl/strings.xml create mode 100644 app/src/main/res/values-small/integer.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-sw380dp/toot_button.xml create mode 100644 app/src/main/res/values-ta/strings.xml create mode 100644 app/src/main/res/values-te/strings.xml create mode 100644 app/src/main/res/values-th/strings.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-v27/styles.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-w640dp/dimens.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rHK/strings.xml create mode 100644 app/src/main/res/values-zh-rMO/strings.xml create mode 100644 app/src/main/res/values-zh-rSG/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/actions.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/donottranslate.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/string-arrays.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/theme_colors.xml create mode 100644 app/src/main/res/values/toot_button.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 app/src/main/res/xml/searchable.xml create mode 100644 app/src/main/res/xml/share_shortcuts.xml create mode 100644 app/src/main/splash.svg create mode 100644 app/src/test/java/android/text/FakeSpannableString.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/FilterTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/ViewPager2FixTest.java create mode 100644 assets/avatar_default.svg create mode 100644 assets/fdroid_badge.png create mode 100644 assets/splash.xcf create mode 100644 assets/tusky_logo_borderless.png create mode 100644 build.gradle create mode 100644 debug.keystore create mode 100644 fastlane/metadata/android/en-US/changelogs/167.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/168.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/00_login.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/01_timeline.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/02_compose.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/03_profile.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/04_emojis.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 fastlane/metadata/android/ru/changelogs/167.txt create mode 100644 fastlane/metadata/android/ru/changelogs/168.txt create mode 100644 fastlane/metadata/android/ru/full_description.txt create mode 100644 fastlane/metadata/android/ru/short_description.txt create mode 100644 fastlane/metadata/android/ru/title.txt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 instance-build.gradle create mode 100755 scripts/import_translations.sh create mode 100755 scripts/xq.py create mode 100644 settings.gradle diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..62eb927 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: tusky diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80cd3ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +app/release \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..39b4640 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: android +dist: xenial +# Disable building on tags +if: tag IS blank +android: + components: + - android-29 + - build-tools-28.0.3 +before_script: + - yes | sdkmanager "ndk-bundle" + - export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk-bundle + - export ANDROID_NDK_HOME=$ANDROID_NDK_ROOT + - sed -i "s/blue/\/\/blue/" app/build.gradle + - sed -i "s/\/\/abortOnError/abortOnError/" app/build.gradle +# - sed -i "s/debug {}//" app/build.gradle +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + - $HOME/.android/build-cache +script: + - ./gradlew build +after_success: + - $ANDROID_HOME/build-tools/28.0.3/apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass "pass:android" --key-pass "pass:android" --in $(find -name "*-green-debug.apk") --out husky-green-debug.apk + - $ANDROID_HOME/build-tools/28.0.3/apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass "pass:android" --key-pass "pass:android" --in $(find -name "*-green-release-unsigned.apk") --out husky-green-release.apk + - wget https://raw.githubusercontent.com/FWGS/uploadtool/master/upload.sh + - chmod +x upload.sh + - GITHUB_TOKEN=$GH_TOKEN ./upload.sh husky-green-debug.apk husky-green-release.apk +branches: + except: +# Do not build tags that we create when we upload to GitHub Releases + - /^(?i:continuous)/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e4f81a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +## Getting Started +1. Fork the repository on the GitHub page by clicking the Fork button. This makes a fork of the project under your GitHub account. +2. Clone your fork to your machine. ```git clone https://github.com//Tusky``` +3. Create a new branch named after your change. ```git checkout -b your-change-name``` (```checkout``` switches to a branch, ```-b``` specifies that the branch is a new one) + +## Making Changes + +### Text +All English text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a Java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```. + +### Translation +Translations are done through https://weblate.tusky.app/projects/tusky/tusky/ . +To add a new language, clic on the 'Start a new translation' button on at the bottom of the page. + +### Kotlin +This project is in the process of migrating to Kotlin, we prefer new code to be written in Kotlin. We try to follow the [Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html) and make use of the [Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html). + +### Java +Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability. + +### Visuals +There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. For icons and drawables, use a white drawable and tint it at runtime using ```ThemeUtils``` and specify an attribute that references different colours depending on the theme. + +### Saving +Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands: +``` +git add . +git commit -m "Describe the changes in this commit here." +``` + +## Submitting Your Changes +1. Make sure your branch is up-to-date with the ```master``` branch. Run: +``` +git fetch +git rebase origin/master +``` +It may refuse to start the rebase if there's changes that haven't been committed, so make sure you've added and committed everything. If there were changes on master to any of the parts of files you worked on, a conflict will arise when you rebase. [Resolving a merge conflict](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line) is a good guide to help with this. After committing the resolution, you can run ```git rebase --continue``` to finish the rebase. If you want to cancel, like if you make some mistake in resolving the conflict, you can always do ```git rebase --abort```. + +2. Push your local branch to your fork on GitHub by running ```git push origin your-change-name```. +3. Then, go to the original project page and make a pull request. Select your fork/branch and use ```master``` as the base branch. +4. Wait for feedback on your pull request and be ready to make some changes + +If you have any questions, don't hesitate to open an issue or contact [Tusky@mastodon.social](https://mastodon.social/@Tusky). Please also ask before you start implementing a new big feature. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..be11d03 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ + + +* * * * +- Husky Version: +- Android Version: +- Android Device: +- Fediverse instance (if applicable): + +- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6a327f --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Husky +[![Build Status](https://api.travis-ci.org/FWGS/Husky.svg?branch=develop)](https://travis-ci.org/FWGS/Husky)\ +[![Download F-Droid](https://img.shields.io/badge/download-fdroid-blue)](https://f-droid.org/repository/browse/?fdid=su.xash.husky)\ +[![Download Google Play](https://img.shields.io/badge/download-googleplay-blue)](https://play.google.com/store/apps/details?id=su.xash.husky)\ +[![Download Testing](https://img.shields.io/badge/downloads-testing-green)](https://github.com/FWGS/Husky/releases/tag/continuous) + +![icon](https://git.mentality.rip/FWGS/Husky/raw/branch/develop/assets/splash.xcf) + +Husky is a fork of [Tusky](https://github.com/tuskyapp/Tusky) that aimed to support [Pleroma's Mastodon API extensions](https://git.pleroma.social/pleroma/pleroma/blob/develop/docs/API/differences_in_mastoapi_responses.md) and some ideas that I may come up with. + +Tusky is quote, unquote, `... a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.` + +## Main changes so far +- Emoji reactions support +- Removed attachment limits for Pleroma +- Support for attaching anything on Pleroma +- Support for changing OAuth application name +- Markdown support with WYSIWYG editor +- Support for extended accounts fields, so you can see who is admin or moderator on your instance +- Subscribing support to annoy you with incoming notification from every post (upstreamed to Tusky) +- Support for seen notifications to less annoy you +- "Reply to" feature that allows to jump to replied status, useful for hellthreading ;) +- Bigger emojis! +- "Preview" feature on Pleroma + +### Support + +If you have any bug reports, feature requests or questions please open an issue or send us a post at [husky@huskyapp.dev](https://huskyapp.dev/users/husky)! + +For translating Tusky into your language, visit https://weblate.tusky.app. +For translating Husky, visit https://l10n.mentality.rip. + +### Head of development + +This app was developed by [Vavassor@mastodon.social](https://mastodon.social/@Vavassor). +The Tusky's maintainer is [ConnyDuck@chaos.social](https://chaos.social/@ConnyDuck). +The fork main developer is [a1batross@expired.mentality.rip](https://expired.mentality.rip/users/a1batross). diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..3f1ce47 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +app-release.apk diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..eb6f662 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,206 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +apply from: "../instance-build.gradle" + +def getGitSha = { + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'rev-parse', '--short', 'HEAD' + standardOutput = stdout + } + return stdout.toString().trim() +} + +def buildnum = { + def today = new Date() + def epoch = new Date(119, 11, 8) // first Husky commit was 20191208 + return today - epoch +} + +android { + compileSdkVersion 29 + // ndkVersion "20.1.5948944" + defaultConfig { + applicationId APP_ID + minSdkVersion 21 + targetSdkVersion 29 + versionCode buildnum() + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + + resValue "string", "app_name", APP_NAME + + buildConfigField("String", "APPLICATION_NAME", "\"$APP_NAME\"") + buildConfigField("String", "CUSTOM_LOGO_URL", "\"$CUSTOM_LOGO_URL\"") + buildConfigField("String", "CUSTOM_INSTANCE", "\"$CUSTOM_INSTANCE\"") + buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"") + + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.incremental", "true") + } + } + } + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard-rules.pro' + } + debug {} + } + + flavorDimensions "husky", "color" + productFlavors { + husky { dimension "husky" } + + blue { dimension "color" } + green { + dimension "color" + resValue "string", "app_name", APP_NAME + " Test" + applicationIdSuffix ".test" + versionNameSuffix "-" + getGitSha() + } + } + + lintOptions { + //abortOnError false + disable 'MissingTranslation' + disable 'ExtraTranslation' + disable 'AppCompatCustomView' // I don't care about AppCompat bloat + disable 'UseRequireInsteadOfGet' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + androidExtensions { + experimental = true + } + buildFeatures { + viewBinding true + } + testOptions { + unitTests { + returnDefaultValues = true + includeAndroidResources = true + } + } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + packagingOptions { + // Exclude unneeded files added by libraries + exclude 'LICENSE_OFL' + exclude 'LICENSE_UNICODE' + } + bundle { + language { + // bundle all languages in every apk so the dynamic language switching works + enableSplit = false + } + } +} + +project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } +} + +ext.lifecycleVersion = "2.2.0" +ext.roomVersion = '2.2.5' +ext.retrofitVersion = '2.9.0' +ext.okhttpVersion = '4.9.0' +ext.glideVersion = '4.11.0' +ext.daggerVersion = '2.30.1' +ext.materialdrawerVersion = '8.2.0' + +// if libraries are changed here, they should also be changed in LicenseActivity +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + implementation "androidx.core:core-ktx:1.3.2" + implementation "androidx.appcompat:appcompat:1.2.0" + 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.exifinterface:exifinterface:1.3.2" + implementation "androidx.cardview:cardview:1.0.0" + implementation "androidx.preference:preference-ktx:1.1.1" + implementation "androidx.sharetarget:sharetarget:1.0.0" + implementation "androidx.emoji:emoji:1.1.0" + implementation "androidx.emoji:emoji-appcompat:1.1.0" + implementation "androidx.emoji:emoji-bundled:1.1.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" + implementation "androidx.constraintlayout:constraintlayout:2.0.4" + implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation "androidx.viewpager2:viewpager2:1.0.0" + implementation "androidx.work:work-runtime:2.4.0" + implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-rxjava2:$roomVersion" + kapt "androidx.room:room-compiler:$roomVersion" + + implementation "com.google.android.material:material:1.2.1" + implementation 'com.google.android:flexbox:2.0.1' + + implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" + implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" + implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" + + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" + implementation "com.squareup.okhttp3:okhttp-brotli:$okhttpVersion" + + implementation "org.conscrypt:conscrypt-android:2.5.1" + + implementation "com.github.bumptech.glide:glide:$glideVersion" + implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" + kapt "com.github.bumptech.glide:compiler:$glideVersion" + + implementation "io.reactivex.rxjava2:rxjava:2.2.20" + implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" + + implementation "com.uber.autodispose:autodispose-android-archcomponents:1.4.0" + implementation "com.uber.autodispose:autodispose:1.4.0" + + implementation "com.google.dagger:dagger:$daggerVersion" + kapt "com.google.dagger:dagger-compiler:$daggerVersion" + implementation "com.google.dagger:dagger-android:$daggerVersion" + implementation "com.google.dagger:dagger-android-support:$daggerVersion" + kapt "com.google.dagger:dagger-android-processor:$daggerVersion" + + implementation "com.github.connyduck:sparkbutton:4.1.0" + + implementation 'com.github.piasy:BigImageViewer:1.7.0' + implementation 'com.github.piasy:GlideImageLoader:1.7.0' + implementation 'com.github.piasy:GlideImageViewFactory:1.7.0' + + implementation "com.mikepenz:materialdrawer:$materialdrawerVersion" + implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" + implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' + + implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0" + + implementation "de.c1710:filemojicompat:1.0.17" + implementation 'com.github.Tunous:MarkdownEdit:1.0.0' + + testImplementation "androidx.test.ext:junit:1.1.2" + testImplementation "org.robolectric:robolectric:4.4" + testImplementation "org.mockito:mockito-inline:3.6.28" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + + androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" + androidTestImplementation "androidx.room:room-testing:$roomVersion" + androidTestImplementation "androidx.test.ext:junit:1.1.2" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a05994a --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,71 @@ +# GENERAL OPTIONS + +# turn on all optimizations except those that are known to cause problems on Android +-optimizations !code/simplification/cast,!field/*,!class/merging/* +-optimizationpasses 6 +-allowaccessmodification +-dontpreverify + +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-keepattributes *Annotation* + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# TUSKY SPECIFIC OPTIONS + +# keep members of our model classes, they are used in json de/serialization +-keepclassmembers class com.keylesspalace.tusky.entity.* { *; } + +-keep public enum com.keylesspalace.tusky.entity.*$** { + **[] $VALUES; + public *; +} + +-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type { + public *; +} + +# preserve line numbers for crash reporting +-keepattributes SourceFile,LineNumberTable +-renamesourcefileattribute SourceFile + +# remove all logging from production apk +-assumenosideeffects class android.util.Log { + public static *** getStackTraceString(...); + public static *** d(...); + public static *** w(...); + public static *** v(...); + public static *** i(...); +} +-assumenosideeffects class java.lang.String { + public static java.lang.String format(...); +} + +# remove some kotlin overhead +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void throwUninitializedPropertyAccessException(java.lang.String); +} diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json new file mode 100644 index 0000000..f1f83ff --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json @@ -0,0 +1,275 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "69e310ef98c0f305934d25e763ee0140", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"69e310ef98c0f305934d25e763ee0140\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json new file mode 100644 index 0000000..fe3fb45 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json @@ -0,0 +1,515 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "f5e93302cf53d4250e455b701bea102f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `instance` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `instance` TEXT NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f5e93302cf53d4250e455b701bea102f\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json new file mode 100644 index 0000000..c217590 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json @@ -0,0 +1,668 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "d4d3d4c683ab7f681459b9edab92301c", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `instance` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `instance` TEXT NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d4d3d4c683ab7f681459b9edab92301c\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json new file mode 100644 index 0000000..ba7e57b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json @@ -0,0 +1,656 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "9a63a3ab2c05004022c350aab0e472c0", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9a63a3ab2c05004022c350aab0e472c0\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json new file mode 100644 index 0000000..85c8028 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json @@ -0,0 +1,662 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "b9ca62605345d229ced2bb0c1f2db79b", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b9ca62605345d229ced2bb0c1f2db79b\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json new file mode 100644 index 0000000..eb57584 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json @@ -0,0 +1,674 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "6a01315ce9f7d402cb61e611140e3c0a", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6a01315ce9f7d402cb61e611140e3c0a\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json new file mode 100644 index 0000000..a4c4482 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json @@ -0,0 +1,680 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "821df8c72aa78a288b4ae9fe2df21dda", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"821df8c72aa78a288b4ae9fe2df21dda\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json new file mode 100644 index 0000000..15a7b5e --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json @@ -0,0 +1,686 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "4e6bfccf6ec0812dc0bc58d5bc8cf556", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"4e6bfccf6ec0812dc0bc58d5bc8cf556\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json new file mode 100644 index 0000000..0319e67 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json @@ -0,0 +1,693 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "33d7d9b8ba14c87b96ce795c337bfc57", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '33d7d9b8ba14c87b96ce795c337bfc57')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json new file mode 100644 index 0000000..0d62b12 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json @@ -0,0 +1,711 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "84ebd39cba4d6749251d330851b70e36", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84ebd39cba4d6749251d330851b70e36')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json new file mode 100644 index 0000000..ca6e30a --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json @@ -0,0 +1,723 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "611700a54bdc155d6bc9d87b8b2af2aa", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '611700a54bdc155d6bc9d87b8b2af2aa')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json new file mode 100644 index 0000000..f1b7406 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json @@ -0,0 +1,735 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "f0db2430c0e36a26264ffb4ac5bb20de", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f0db2430c0e36a26264ffb4ac5bb20de')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json new file mode 100644 index 0000000..a30ff7b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json @@ -0,0 +1,741 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "8d27bf5cb75301211453986dccaf2c57", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d27bf5cb75301211453986dccaf2c57')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json new file mode 100644 index 0000000..771f177 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json @@ -0,0 +1,753 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "0cb482507cdcf5628ae028242c3b74bb", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultFormattingSyntax", + "columnName": "defaultFormattingSyntax", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0cb482507cdcf5628ae028242c3b74bb')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json new file mode 100644 index 0000000..7c98d37 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json @@ -0,0 +1,759 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "90a7a3288df43c1f177c54c013629b0f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultFormattingSyntax", + "columnName": "defaultFormattingSyntax", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '90a7a3288df43c1f177c54c013629b0f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json new file mode 100644 index 0000000..b7213d1 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json @@ -0,0 +1,897 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "0a5f8f196d357a01b8b571098ea32431", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsStreamingEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsStreamingEnabled", + "columnName": "notificationsStreamingEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsChatMessages", + "columnName": "notificationsChatMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultFormattingSyntax", + "columnName": "defaultFormattingSyntax", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, `chatLimit` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatLimit", + "columnName": "chatLimit", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `pleroma` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pleroma", + "columnName": "pleroma", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `lastMessageId` TEXT, PRIMARY KEY(`localId`, `chatId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "chatId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatMessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachment", + "columnName": "attachment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0a5f8f196d357a01b8b571098ea32431')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json new file mode 100644 index 0000000..ed54aec --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json @@ -0,0 +1,909 @@ +{ + "formatVersion": 1, + "database": { + "version": 26, + "identityHash": "f6370dbef6f97c3b6de019eb14c7c461", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsStreamingEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsMove` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsStreamingEnabled", + "columnName": "notificationsStreamingEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsChatMessages", + "columnName": "notificationsChatMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMove", + "columnName": "notificationsMove", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultFormattingSyntax", + "columnName": "defaultFormattingSyntax", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, `chatLimit` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatLimit", + "columnName": "chatLimit", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `pleroma` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pleroma", + "columnName": "pleroma", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `lastMessageId` TEXT, PRIMARY KEY(`localId`, `chatId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "chatId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatMessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachment", + "columnName": "attachment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f6370dbef6f97c3b6de019eb14c7c461')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json new file mode 100644 index 0000000..d758b15 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json @@ -0,0 +1,989 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "8977aa85e5ac4f803fe64b7e04ef4eeb", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markdownMode", + "columnName": "markdownMode", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formattingSyntax", + "columnName": "formattingSyntax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsStreamingEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsMove` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsStreamingEnabled", + "columnName": "notificationsStreamingEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsEmojiReactions", + "columnName": "notificationsEmojiReactions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsChatMessages", + "columnName": "notificationsChatMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMove", + "columnName": "notificationsMove", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultFormattingSyntax", + "columnName": "defaultFormattingSyntax", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, `chatLimit` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatLimit", + "columnName": "chatLimit", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `pleroma` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pleroma", + "columnName": "pleroma", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `lastMessageId` TEXT, PRIMARY KEY(`localId`, `chatId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "chatId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ChatMessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatId", + "columnName": "chatId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachment", + "columnName": "attachment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "localId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8977aa85e5ac4f803fe64b7e04ef4eeb')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/keylesspalace/tusky/ExampleInstrumentedTest.java new file mode 100644 index 0000000..e69de29 diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt new file mode 100644 index 0000000..9c65aeb --- /dev/null +++ b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt @@ -0,0 +1,64 @@ +package com.keylesspalace.tusky + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.db.AppDatabase +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +const val TEST_DB = "migration_test" + +@RunWith(AndroidJUnit4::class) +class MigrationsTest { + + @JvmField + @Rule + var helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + fun migrateTo11() { + val db = helper.createDatabase(TEST_DB, 10) + + val id = 1 + val domain = "domain.site" + val token = "token" + val active = true + val accountId = "accountId" + val username = "username" + val values = arrayOf(id, domain, token, active, accountId, username, "Display Name", + "https://picture.url", true, true, true, true, true, true, true, + true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, + false, true) + + db.execSQL("INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + + "`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," + + "`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + + "`notificationsFavorited`,`notificationSound`,`notificationVibration`," + + "`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," + + "`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + + "`mediaPreviewEnabled`) " + + "VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + values) + + db.close() + + val newDb = helper.runMigrationsAndValidate(TEST_DB, 11, true, AppDatabase.MIGRATION_10_11) + + val cursor = newDb.query("SELECT * FROM AccountEntity") + cursor.moveToFirst() + assertEquals(id, cursor.getInt(0)) + assertEquals(domain, cursor.getString(1)) + assertEquals(token, cursor.getString(2)) + assertEquals(active, cursor.getInt(3) != 0) + assertEquals(accountId, cursor.getString(4)) + assertEquals(username, cursor.getString(5)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt new file mode 100644 index 0000000..2f891b3 --- /dev/null +++ b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt @@ -0,0 +1,249 @@ +package com.keylesspalace.tusky + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.db.* +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.repository.TimelineRepository +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TimelineDAOTest { + private lateinit var timelineDao: TimelineDao + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() + timelineDao = db.timelineDao() + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun insertGetStatus() { + val setOne = makeStatus(statusId = 3) + val setTwo = makeStatus(statusId = 20, reblog = true) + val ignoredOne = makeStatus(statusId = 1) + val ignoredTwo = makeStatus(accountId = 2) + + for ((status, author, reblogger) in listOf(setOne, setTwo, ignoredOne, ignoredTwo)) { + timelineDao.insertInTransaction(status, author, reblogger) + } + + val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId, + maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10) + .blockingGet() + + assertEquals(2, resultsFromDb.size) + for ((set, fromDb) in listOf(setTwo, setOne).zip(resultsFromDb)) { + val (status, author, reblogger) = set + assertEquals(status, fromDb.status) + assertEquals(author, fromDb.account) + assertEquals(reblogger, fromDb.reblogAccount) + } + } + + @Test + fun doNotOverwrite() { + val (status, author) = makeStatus() + timelineDao.insertInTransaction(status, author, null) + + val placeholder = createPlaceholder(status.serverId, status.timelineUserId) + + timelineDao.insertStatusIfNotThere(placeholder) + + val fromDb = timelineDao.getStatusesForAccount(status.timelineUserId, null, null, 10) + .blockingGet() + val result = fromDb.first() + + assertEquals(1, fromDb.size) + assertEquals(author, result.account) + assertEquals(status, result.status) + assertNull(result.reblogAccount) + + } + + @Test + fun cleanup() { + val now = System.currentTimeMillis() + val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000 + val oldThisAccount = makeStatus( + statusId = 5, + createdAt = oldDate + ) + val oldAnotherAccount = makeStatus( + statusId = 10, + createdAt = oldDate, + accountId = 2 + ) + val recentThisAccount = makeStatus( + statusId = 30, + createdAt = System.currentTimeMillis() + ) + val recentAnotherAccount = makeStatus( + statusId = 60, + createdAt = System.currentTimeMillis(), + accountId = 2 + ) + + for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) { + timelineDao.insertInTransaction(status, author, reblogAuthor) + } + + timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL) + + assertEquals( + listOf(recentThisAccount), + timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() + .map { it.toTriple() } + ) + + assertEquals( + listOf(recentAnotherAccount), + timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet() + .map { it.toTriple() } + ) + } + + @Test + fun overwriteDeletedStatus() { + + val oldStatuses = listOf( + makeStatus(statusId = 3), + makeStatus(statusId = 2), + makeStatus(statusId = 1) + ) + + timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId) + + for ((status, author, reblogAuthor) in oldStatuses) { + timelineDao.insertInTransaction(status, author, reblogAuthor) + } + + // status 2 gets deleted, newly loaded status contain only 1 + 3 + val newStatuses = listOf( + makeStatus(statusId = 3), + makeStatus(statusId = 1) + ) + + timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId) + + for ((status, author, reblogAuthor) in newStatuses) { + timelineDao.insertInTransaction(status, author, reblogAuthor) + } + + //make sure status 2 is no longer in db + + assertEquals( + newStatuses, + timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() + .map { it.toTriple() } + ) + } + + private fun makeStatus( + accountId: Long = 1, + statusId: Long = 10, + reblog: Boolean = false, + createdAt: Long = statusId, + authorServerId: String = "20" + ): Triple { + val author = TimelineAccountEntity( + authorServerId, + accountId, + "localUsername", + "username", + "displayName", + "blah", + "avatar", + "[\"tusky\": \"http://tusky.cool/emoji.jpg\"]", + false + ) + + val reblogAuthor = if (reblog) { + TimelineAccountEntity( + "R$authorServerId", + accountId, + "RlocalUsername", + "Rusername", + "RdisplayName", + "Rblah", + "Ravatar", + "[]", + false + ) + } else null + + + val even = accountId % 2 == 0L + val status = TimelineStatusEntity( + serverId = statusId.toString(), + url = "url$statusId", + timelineUserId = accountId, + authorServerId = authorServerId, + inReplyToId = "inReplyToId$statusId", + inReplyToAccountId = "inReplyToAccountId$statusId", + content = "Content!$statusId", + createdAt = createdAt, + emojis = "emojis$statusId", + reblogsCount = 1 * statusId.toInt(), + favouritesCount = 2 * statusId.toInt(), + reblogged = even, + bookmarked = !even, + favourited = even, + sensitive = !even, + spoilerText = "spoier$statusId", + visibility = Status.Visibility.PRIVATE, + attachments = "attachments$accountId", + mentions = "mentions$accountId", + application = "application$accountId", + reblogServerId = if (reblog) (statusId * 100).toString() else null, + reblogAccountId = reblogAuthor?.serverId, + poll = null, + muted = false + ) + return Triple(status, author, reblogAuthor) + } + + private fun createPlaceholder(serverId: String, timelineUserId: Long): TimelineStatusEntity { + return TimelineStatusEntity( + serverId = serverId, + url = null, + timelineUserId = timelineUserId, + authorServerId = null, + inReplyToId = null, + inReplyToAccountId = null, + content = null, + createdAt = 0L, + emojis = null, + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + bookmarked = false, + favourited = false, + sensitive = false, + spoilerText = null, + visibility = null, + attachments = null, + mentions = null, + application = null, + reblogServerId = null, + reblogAccountId = null, + poll = null, + muted = false + ) + } + + private fun TimelineStatusWithAccount.toTriple() = Triple(status, account, reblogAccount) +} diff --git a/app/src/blue/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/blue/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..d4bd0e4 --- /dev/null +++ b/app/src/blue/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/blue/res/mipmap-hdpi/ic_launcher.png b/app/src/blue/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4e05718dcb6820ec4da824a75a5e04c6fa0a59f4 GIT binary patch literal 5329 zcmV;?6fWzDP)Plj!AAJ>beYqn(`h_8ZPV#y!gv1oNk0oAf#eXo>C8Llo+Cgk z`f%TU_r0GaqW>SCnGxX^|7V96V74bBiD-^bMEl&1^10tE`Ui%Kkjv#Uc6(Wx$v#)* zC@Np*tf)OyR8c#W=d64!x2WR1oWkCu+izPHWQm^pDC;eB8qfYEHo5X z)I4o+6#rbEo%aWo(ebfDp98th2D#40;e-N|#h8()%H|@AF^|hMmOrSCxxa7}S3HyN ztS}G@HS0b<_9;h%(P&Jt*_?*ribW?ad8NNs868mQbGY#0d{Q8Z(!_;WX@tg-|9eN_ z{8I)?t})$cOrXEBjEW$l*-mGL!BJH4l-c3@fHCGmX|O|Suz#vRVx^G_iOL9-(f*+^ zr|?NzUa^JP;(yh$sl%8=`kA=e|5G5yt?Tl_d<&d(&a zO}M8cYHmqoxi!D+ds<7uSTLMWfIeyc@x}24%8=_JQDu)YCda?q^Ghm;?WsQX5kW*z zg{2FYnR1*zRhx3b7;`bx*>ktk%O6N6(<4J}fJ|%sr8UpFls@H_AcBaZthps~&35N6 z{9yv&s0?Am=d4)j#hEU1 zR0cbCuByX}U)qc94U3^R+Gh+%%4B2hl1e;xWE)yH)FI1*5J+_pNOh2Dt^Z+m z%&qqz&FF|MbS@}0*otlj!U+VlxUvGTcDG>k!h?AGY&%xhRR_lj0u(HJYRgOU#;GQ_ z2V8jTbTjHI%R&MYNOj0$%)ivx3QLJ?17>8*V$9~uY=`q@#+W-k6^s{0o9)2U9h)$E z;X&N=#Jhc+a26GR;_PwQ^KkiW3q}V!Fgn-)_kas$JJ&;Nu!IDZo~0d^YppLcj7dc^ zu)576qJ*5nd8akz{IRK~m2XjATv>rDJslj5H=aMX8#a6XT?3iSb_{-D3x_j0*n!c3 z4qQ9mhE_H!g=<7d}v?%7mtMb$GY89ixLCxY6&z)$?r_9bl2ZeQXyb%E?9AAIBR)neMN~ zoPs7|k5}%hXh&KcB{sda=;zbH_~IE#m)Ig*({)RC)Y;+g(D1ygRCL3&p zrx;`I?NA#*#%RZ>&dnUqrBf|1m~*jmQ6=6y(}tTDx;$w0Vb`AK^>uI$u+!?b?t>_I z7D2Ag#w%ah%K?4u@McIEV@Noj7|T+dPbEqud%f7j9m7 z5byN3Fu$yXTeV7yi*aM1lS6uNTYbpc)4p*Thx1zZ0m#+aTw|DHv*YraW{eKFFx=CI zZS~b5ug|*c0AU!4x>ZtGz?v>I}?DJ&qN zRQD&P##l@tG>{QQ6lby*oeKv?uCu{t&Bb#^cX7q};av?->TMHNtGcRk+#F(ouAFPf z^6H8yRxK82c~v>yJl%}Z!4AB2rWww{!U;ekg&v)omf}Vq3-rRHJ7CJTg$2ZBk@*4< z#nUt^S|nqz7#UOU&w@>(K$}mw&W7B)LR>!Aj++-A#BhHnwyvz5)Gb?c@^GoU88?Tz zaHGEy`_?SsaO4^bR5~jgS_`rm7N~VY9d7iyFgnvD#q6GaVaQprJ#!*6YIMb8T=Bc4BnM6N7i+MxP7A zeeGQD)3Ir3z+(-u!hmNV-pT=;Y+e;MP^y?27fLl}5)%`HF8Pr>u~^EO9IsD>5$M=L zrnTbYBRjYi>TK7hfL*f1mWRI2ZNcc!A)n}+7=yksaZB;SqdVa&EPyz`vBuHeD_EfZ zc6|HTj&O01DrS(LseLUmO`-`l&E^O)6=s#uaVs1cnbr!W-sV}}S)k8tUoinkuCZWQ zbphteV4-X)M^bq868)ZpLWHIFqx- zH?Ev*LHmX}$kZkdXhBI42juQ=M|n|USlQ!=j|CFVIwFemuS2tlC_!z?IWiSQu=AXT zMU@*_P7BG4nb~Y@+)t>gdn^u!>TwL7fYsbSo>Y-3)LnzZjB-7)1Pb&ws zp>Dz503k!BJx&33`Ll^AS*f>uGaMLiY^kr|`kQxqU686Ru-Wo(eE({U40MKpGj-

J1y>rNd~e~XBC{&gsdh*DJs`;~BDywS2@ z83*+CnO3L`IXKa@7Q=lVcaJkg_IQEZ11?@- z<1g=P2ph*!sNW+Z0r_n^qKQZ#(^)?X6~#Bc-~k#PI)oejofsMH3psYy=LEpHdae~ed}1Gdc#@4vPanYBr|x`Nfq>X4rnbBnDvEEktY10_#y!}H zXOC=0aX}%J8gsx)#@?tYcVf7=Z352It!t2}G6cLzDwOGL$~%Qxv+T8QO3bZxG~NM9=p z)YpcQ-d3F0-+)YoE-av=bm_@JjpA$~N)co-Yco~ZXz!NQN z1EPqPMu`1je9v_()*O8QD|>LGuMPWF*9Og;V8<8&sRsXYbSvC_ZCqSDya{TpF=QN` zNHw0EmbsOPQps0rE}(OdDghqiG*BmEtyt1JtMBT*W$zHR}U z)-J~UlA;L*UKw&dWQ-BtII#P{~{ARA*+bFKx$J*{~D=r(Be z<_S1ynHrc(7F<2s%mIa)K`9x^%TaO3Im8~`PdZhMCL&3OR68J67{`O5_|_IFlZ`L# zZQvTc;of#Q=lWlDV57Tj?S!t__sIs{>9lcaC0>4PcfiXwuY6?>x17KAOcUli=T7RB z4F)sbIokwxUn@p>TkuqKBc#gEY7kGP8cz|(28m+i{wtVwL=q7%O{8oRD~%roL-CJ2 zt7`FXZyR@Ryyb!FfI!>ot8wjoD@F%gc;)fEu;e4jwzu7_iVX_ zK^cDhOcU4So#|K~un0>TBdSV^v8S;H1$lV^nm@K%&b2!N7Gs&pfX146*il~vqtOy@ zpLKNCa=80gpett%V%Y*`IMeHc*jQe1p!(xGVu(m2%wjH1h2jgKHCpkV9rWU1BoU>?Ckd)V za{ZsC>Ro)JePbO5BH3#&ieGOqvCxKLl z9Ge~g_T^o;cD@CB8*3nv>871hz7KNq()k=v-=TFO;;4`!QhXSfm{LbX=@jC#UuStF zmFW^ye}+PTI}nI}RF@TFxTg*70T&)S&=^*&QK(Hg?OKcLy=@rjYs00d_F-9NX;=sD z8<&*B-PelY^UYXMRT}EjK9-c8)lU?yp)j8QLd4EPeq2&|jX7(`ZB8e4k?r7U|U|_Ck;q`qhZ~N+;ZXt$6A9b{LK3&e}+KP6G{wY+UbY#*@vB$O>VD$`diS<9Qj65>Y0FSbO|OBoU<$k(w`33<;&WkAvZO zV^3o(-ag#~MrRHgCzL`Q>XF4u$F`$sb0B>o;E zk`lY6n330OM0kEsG7-sQQUvSx67~DONL~QJ48Yti&XgfifhUx|ACs86jffP)*47z) zSyY7Q2l9wWo{%EkpC(cNIxSNZ7RWtC@tsx~ir>a2rSB&qC9&IyPvtdZUZfNvlE)|U z*YYytucu|I$3lU*KRC* z=F`MU$xz)+NRwQQjES!xcAqul(`q`TL}HHzj0vg2M|c^k-|{8uv8gcb#dP9{RAb2* ziVxzG1;>fVNbG8rUFMd8HLw<`VQvHP|QjPOOs+pQb z<2;e-FDXL#t$4oVTeIiHEhQoYv9%_R*uL$aPN`7FEMkjb0D{?EDt05vSvHz5G&+q=b ze;1fp#QyiGcG@$ z#EK{O!SRT#7eZnmw3yf>S>m}Z@_a4LbKhjo_r}m2l)tMQGgHI&4c?-NofDl7#6WpVtx z*zowqyW*MQzXtnqZ!uivlt0=}e32CKMN(xPDL`(PCoZP19B!6h@P-m^1-a}Cnc?w! z^2PBO1gMTP{Ms811;=sPRH_&o9{12jb;$1~?K*haD?H~$GQH( zw^O;{1cc^)#-S7d)MM+<-=55mJ|B$dK44;*GR|rOQT`aA{4wHU;vcZ_%wO^&=Uxp! zjWbwwYtrc=l^>b7J(4KA%fvFI6zu?#T3*#a4qucwd{N?JYYV#fJhwXJPHRAB*=F zpAW{fgypiN-PzM>c~t}1y;0)u#fUeYd@-3TZpI2P2^0W2li86$K3#mF8fGrfVo=dO*V zhW=a&XW7|P2dLHTsRv~9M2L&!UyP&*YauKH+A=y=o_FYG+R?Kvs8n z!52%JS}^Ye|$su|8%1VrrK=sd%v9{`}kGS*270Nweu6IX@e zx%a9wN6wVqN_n*ls2Zmfc5mc;u7BWq2!~f%YTC*~W+W9&4*j(mOv^aI)^K9+6uI$= zEoA?fFDD=0Fh<-ACpmjcVR3~B72sZFW0@3M=tl~pF`$p73U{+y_N1J$T5e-*ghL17 zB%jTZ1A8wgb2C%q=*%AShx>MsD=*wcJS-49i}BRGpCzCE*m_ynQwqu-d({~T4MWCCy8}Q^A~$k> zEtFPZ+L1l+zJ7A(#%-tJ9G#ge$7L5x5IPW(jAN#Q#7qZ?H<&yKKr1ziI%E7mKFsI; zy&5=DfwpcuhdlHB8yW&Vec$KE&aLMWH^a#SvU?)$1VY>}BrG5m05tLR(0#4q$mu(c zaH@fV(FD2W+Kb4u5A3cBG&eIve)rwalC2xZWC00T#2*6CgC;YYl8q%eHlF=UEets@ za>}Y+=0vy@$)t0H7N_)JG)ca*`%>~;8ENV?pt+eTa_Gj(Wdm8=p?`ST7>6b^T6Z{} z+sVYzC*;7iv#d%$1GxcmVDC0^_|_}Qd1E8xbx$ywAk#ZPL7ut)hMG8YGgIW2Yc7<9 zV{)>@OtbG${>Tmhb-_NBb965~o?;DdY#p!#C|9b2EF$+{_;G*d5oB ziM7M!1tA;Z$<5bnC4c+iPaT;qSr!)>XliS&E@AAE^|ySOuKMOWmS!H^uQi6H#0>J-?EdeA1#&_ zgiN@POz+%6p1ywgX2Yp1zDVlSO5il>JA?TG z`O#N)h&Z2L3M9^0bNi>rW4B#P&M6L+kpkg9a@pnyl1XP}H|9zlF;3a&(4_!0)qSV~ zfHoNK|4Ji>rYI7!rWQ^jnIQ-3WKOLq{qZ}mBV)sZgsQWxSOySrc*D1%#x8#i0DT~u z`flDP_*VdS%t5 zv>w_wNrndoS}v_dI3_1cEUwTA0FLUJtQ&yC6H3gt0wV`!AU9BdwrYYiJH4IcGqw9p zyMRn|@B;u8gcWspr~pvzU}C-*L^G6nFmxbZcM&KB=g2p&DwkEOeMb@y#mob6)&g?4 zgZznBQ5wONwhxOq&1S3W%$X#`I%U8Mk_L0ICafq1?zETg!?hWzJNWo|XQhgk!k8e;`O)}?~-?)kl57aKJc1-Ctfn4mf zVh>ui0yI@MkkXyx>-8b1X&T@cEl>Zk1?A6nJCjgVf6TC$(N7IRU>n@*PY61;k2voECR9rQt8d( zsIgg908n>nwNa-(`jlLh(izWuHn(`*s$tgD+FMO7Sp+sKtG4zXF$|8tQ=J-pKZJw# zryW)720%MJ;Tfl&TaW@$8F9X!9N2rgWR&W?RlV)QO81<~@-o=G2LP3?dIl>EB>6#8*Hu{?cT`A>dV_2 zMZNwa2W2H;gzRBY_81*E18_lN@2MV200sa-Gvz;M_e4%D2c{m*YG;jDTBmdt_bh-A zgmYjm-{{f}z^YXD7R)sJ8=EJx>M-pFGE&Tf-sb&{Ql%Y*aC&E1Hwc$Xjp{L4F1EOW zFEztx1R|%L+MP8b4$<5F|5W!_E(SnDW@D~5l;RV)wMOa+3(dZ4b%$lcoM{{r#Snwt z_libmz8ZiB!j_^f_iHG`C(;A(stmSE&2;dUN~CtetQ3yX?t4YmYuXOL523fV^|mO5 z_=H9PKCQuiorMnm-Qo(#0$Ej*%Cd6!UsLO>*8vDX*iNj-J!2�{~x-&iqNE!}p}c z6`Yp>b7pV?^Lm@-x2j(AWe_sQh-KWHAr+}pL~<)t+EKmL^PrgyzTXPWO5zwC%t@_< zeo(3E8HcdXYI|8VL+XWac!1NI?9)aE^O~6s&NsqXjdC(l%)H*=f4xUB%77c`DdZ4>ow@GJlA2L#bmgC8=` z0b!wK06+ts$u6yatIk5t8Y%zVCMUZ(Fb?0_y=LblU25G$KxY!d_Q5FrZD;)RJW9&| z4TKYJ9ssc}P48yC+4+dU;X7uenD>knvtXo{c9qeBkz(F6*nP)(%=FsU8X}#p;3Z;*1dbg&o3+c0!nvY4K}H{6CBMSiShODlDR$RXwI% wqw)jZIw6#XN({9arSD50^rjRazv>|W0g{tnYz=dnNB{r;07*qoM6N<$f@^ZOUH||9 literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-mdpi/ic_launcher.png b/app/src/blue/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ceafe8fe98e144232ea6cb7ef6fe5a5714f0e61e GIT binary patch literal 2839 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 literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-mdpi/ic_shortcut_compose.png b/app/src/blue/res/mipmap-mdpi/ic_shortcut_compose.png new file mode 100644 index 0000000000000000000000000000000000000000..a97038f75b64884b8b461b3a5f424a12dbe8918d GIT binary patch literal 1837 zcmV+|2h#Y7P)#K({nXNIpCa`>8|#Gxe0vXt0X6UUA!wN6?HjTFFXoi-L6OD>R9 zYLRt;21pi;TV&(bWwK2-ZV@B_S}2RAK+}bRpi+XsP5y*BkRPbw)5Q!W@|uqsl1jSh z1rA^clJ)xz&&*t23i$u3Sv>$80D1xR0q8fY&-|Lh{GR`*<NB5e#o6}@laIbWJ9qkNUDv%Z0)1nv|B-J!xmcJ!{;oPc zb4SjNlawt|Jfl-Qqf;zhFe+tKR2ieFGDeZ)C`FQ^q-do(YT@X+Q;$5h0OM8q+FqId z=@X~V7N(AWlGG+i&W%$AZzn)FkvA%tr}Ws=C;IfUvu2P3TaMYeQ(E!J+?Jx1D4|YJ zLY?R+fSAxoOlTy?Dv1e=(qmIwh0?5s0~oEo(xZ?6IH%8il~gAwsZQDo(5U;>0R&kk zUdmB4J@%D0G5b7>3%@Jt(8Tm(zfR?+s2#lB0KAkVK~^bSIC2Gmvs1jYI6Zew(Mo&x zgANdi?^k-X^t(<%^cSb+F1L+m0$A;S1wRtY(nu^zsr=++v~h9wjvqboLMlJ?pc(!k z#)l0s63aeFpy z`Wb+sMtFx>m|kiG?*v~R;Hgt{bmO-#KNMiQyg}!_cdBUse>g=kCBM|@RXCg;o%$1t zX5m)xZ1>MTb((VdB8dsj2yyl0J$xm=d#_z+86XtT{t>`o7T*(%M#ZEyanB085x&vr ztgS54c6pPozq(4fe31lMr87^Qpc_|T+QV;`H)wTfzA2s)6$(bucm4iI7|b21H>FK1 z*F&>#JNQU)lrAnC_?@?my8h}aWi_36DMx2c9;X{uU!v{u2Cc6wwCp}7D#VEjh2p7Y zc8m@sa^qKQ!7bp|R=!h%ugv}RU!A9nS|DD^(RWWCrx%~GjyGNfM6)1Mdd@(zAQ*as&2dOQJOj7V zd0V4nmXDXW0C=mP#zTUXebWlMQfn)VO}pN1=UaDQAJ)E`Pv{)G584|e0AW6!y;%)r%Lq^(f9cu98hE$yUS7TlAi|DM^%%Lm zq1d`$9`CV79=w#Jt;-i{(EFWVum!h^=fotrIAI-Co49(bA3!)1O@CaCuC71)@+b7+ zm9?5$TRv#KLLNT;53~DCPw9F947ohq$xu9Vn?0+nE_PI!!8^rsLgKd5CG}92>3f?ZB6OuISkGumw1~6!4bn1n00g!#6_}hU40L_Zq#hsy(Q?h?>nGJY087_)Bq9^cL7 zfN_5wHX#uhca97I6+qrB)%;q*{GOosGfsTNe=kwJ958l?hhSWQE*Nk7hRlBtzySB5 bi|GFXraXb^*nTuQ00000NkvXXu0mjfeNlgz literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-xhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4dba57cba5a62cbe7422fabca53eef60bb521c60 GIT binary patch literal 6418 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 RG45XnpsA({t5&g({U7+hBXNkl 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 Ye_f&Xg}#BmVgLXD07*qoM6N<$g0h`zZ2$lO literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-xxhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1e231b6377a70851000a458cc23a64a77abac19e GIT binary patch literal 12200 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 a{o}E(##cQ5otvRS0&m}_%U8&nhyD*d{b(}) literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2728f33cbc1fd37cbddf98945eb5717ed562652a GIT binary patch literal 14987 zcmZ8|cQo7o_x~$m6MOG!YtPnbDN&``DzW#fw$v&uYA3d;qN=K@#HhV%@7S%jwi-1; z?HL3?B)@onzJLABLC$fK=j-{r_i-Qh-p73=)<9neLd!!70089vJuM^BE9&Bxnv(R_ zXvUoi0EB@1TDML7zihWU``ApRtTs36C28F?1r?H;3^1EUX$rp_ND{jJ;OnPct|S_X zPyI>n?`glkcjc+Ie%aW^r$4_vvHte0o4iuZEx&aRclfz#9$Pc|_HW(Mnb&5P%=6iP z<|i4o1bZ#A$nWzAWb5(NDx1d3tLFdzkM_&+JD(nuFFnBZL>>uppIW_kgd-78PO9Ha zHaa#h?RJ;ETGOpn{^-}dst9T-b3TXq zVj19L!}SZw{2{!RtrP85L6nHm>1smm$MP_Q>$=UEk8$60Z1kVG-xnW$`t+%US9+ui zgCp(f^9}{AmseB!a_;QX;Y@&g*VDdG{Ps{gFH>{db>Frl#wWBv>|bF$@zCG4V2i&a z5h)OU3#-|;3HyNxs*;BG7SRtLGe4+}5*xN?)z!P7?UyxR^hQL7V*2Ms<=1aCk{i=i z`Bikhf6uCFgD}LJS7pR+d2ZXfdLAKtZt1FLdLquChWHLA>QjcK8UFV_C9%J#k!EIQ zxvo1l&ADaHUQN}0|1{kO152N8bdGE|Nl6vmwtXbtfB5Rdy1Upo$Ek@BB_^LNg@Vq6 z<3nLtq0-IatwOEU&y=O5yQutx+R2gH=h=9VnA{dZ3J#w^J% z^E}gMm0yc#AayT4Y;Nna6@!911wmlBJkoyc%wiPvY$sv|V;KN=hRVXXfHBu~6>b89z;H$ez$qpj8YoaKM$oL!@(7mF83M zs-7ZjCq~rD$EUS4*axquJ>Yr7Eyc2zb`nAhd}Ze}e>NRR1@pst;Lkr#D~(y{s-fB~ zAwou6hF|4!Osrog?5nsx+a-qFaT^qOdr?IxcKKaS+VjXKt{*hw(l(^R?QQ4Rx3=an zMs9`Y@2Xr6e_S~j~K9h1U-+$&m%#vyC1OfY%N>Nj>X3FpF>NF-O9l8 zckh*RNjo)FTg*sEe{4yZ>nPZ;J|nnFm)k{VmOs-b^8N;dqnM*!f@zm2$3DeX<&HQ4d3|p&9~`4))ykpEG<3PHkuC)n?Bl&S^jM46dc4lH!cwg^gs7VHxqsJW|M7Xlvzp7GU0F=la($T@jLRsXXTXhE3Uk+Txd5uAU@;K5E{+B_ejgy z1)@}Q2zRJAUk?jWzV}%uvS$nP&IX#>Snc`Pdhircf1||ksgI0ACS?QnK0*o1Bo1LH zb=f#QeiL06|E?6$Y4_`cvm)^I!C~N|pn0^9t*)%Ats#^do#=d;EJn%n9V=$`xw58- zX49&<02lmJpg~ppJ}Y?@H6Hsxv^A&h56N(k<}8OB$nnb#ps>{;KlY zRaZm96bLYKDUF4qW$lFE*grZw-dj_CzT4grW#0PPa+>HDW8Za=-e=(afP_R6Dm8NYw)~#v7Zzd>*`=&u1QPz z+|lht{*UFCaA#ug#2d-a79EMESlAL)G-)3pg7@KecdKlxT~%zf(TCpF|3iVY+sePm z_q1uCtwZ*nC)5O&8iY-BA_(e&F?oR(SSin2r7PZdd#L=Ml*?q)u*NuSq1$$ z@82rK(hy(1CMdfS1mA#gMr!>&N~JD4gp{^QEr@R1zw`le9~`HuTGZ968%LRk?bjwJ z<7S^=?bT1(m6uDTEJh~-jMGEe3Ezq-y`>iHcPQ_TVf&Dv{VPq{te^OwI%TP7DW}lf z8WC`N5tUySbYAff)IMhvX?^;)s-l+W(8(EvkzK{=l*(mmxpYPs)6R_ieJc`od1IY% z7U(87h44?*eiW9jOh-rCY+;V+32|+eGqU($In-nVsO6d;z3lIY=%0*R+B$}oojOD> zNY;qu-ui;|UKeLHW_n93Goyi@?nC}39wAsx7g;TP32#&Rrcczh2AYQ&42>KMvQknh zwTGl->%ryEIxFNRU#yI*%k z+vEU|58}`pnmmd6N&Z#Urz?Y1ZUM#`*V9rrcPQ8chBwY7NFUbXeoVb!1WVc6N`R_` zux*xCki z?t>Cp44U8m1}V2Zktu$|h40|faFggv)!#Y_C7$Z~Egr`8?Va}g!<~%XFf%imk)y2q z8@J~?$hQw5%NaA(-UcR1i>xymU(0}$@T%IAg}P&INm$grOAc zEbaI_@)91E9g2FI?^{FPw&>*xRE$AF)pd>Dwl2P#^Z!85`yuM-`wv6bFr$YnKfr}b zDjz+tsl~r;1WhQ}+<-2;X9WL>|2IBmI5RiT#|d8Ics6Wfa&o-ipMM(=su+{DW*rJT z7YE*1nwgo~OpF`|Rz)M9hjJ!(Fml1avCdRgsb0!Kh*3_?jJ{2E?!|ZQUPE=x+-M_w z7x*Bi7Ps(hiTLopMJJNjDELRj>Vi_w#UMr98}DUiMoz3*loEPkRw# zy3Fslxn+g!Q`(5AfpFv(Zd$3+QTj?benaeHxNMYX4adK;K}ISFbl6&yG3U>ch0?HnQS~CDz+YGd^afb! zSA6d1!dsWqJLYZu;iN(c+TY*5h`P2xU&Ya?x82$mbye|2gnfDazYe|D2CqH;dl=T) zR%fp%En-)_Zwv*IbVhi{u@zg-jq_|>7qQO%>3n93yV_McbM_`?P9^SZIOAIjshiul z{qrEr1cFw3@FOl=-Jh)Lr`BcKxVY7XNRQBZq)Mn&gknAk9Df#zp=s_t+Gv?KtaSpI z?`J2$a68=zRKdc`3M~aZAH?HY+?7C>kQL~QqD#i*CdW7moJT04coVn%YHxuR06P4R zRo3<5^+YIm>AsJJ-NdQk-Mf^GEDZtcLkmEI6QjJr{`q-Yg91jOEBbtf>~*yTPq`Q zPwkyo5tG{CeTvXun$C^hdbWS<7irpUJo|k4f5R8-N#Na7{p@vG&JWrq_q+uM=(exY zaGpa%n%xHa+CkHs#g)aS^cBAoP83l?`Q;5-2**&;`k&uO5FBec*zb_#YhF2(%~$8< z=Dztm1Ke(s{-%ozYJ*Fmq*9l!nW_NI1@1GESaK|lAJ_Yjifo$V$aC#M!&*C{vdeSB z`{^rsMvpCXMsWy++)!VwoVI(V3?yPz1YU zJ@{hZ)mjSWeBtrjDYJ0FO#VX}B0lPVOKU~Q-q-s4Z!FMsoZ51@ELlo`O_B=v$jk_!?&3KZJ3Y5W1OKt|m(%t*hy@pvv*^z6s z!*ug(z<0L(<&pjAKl4$v@ULhRNE-a^q|WCYpRejv=FF&JMJA>&J8GL`_o77huxl&t z780LeWeq;>B}MRBpzU2rPq=JhW%F%mKW_L*mmqRxmmQvCtm7GPeR?M~ui?~>EbSp> zW|6@!h=y#iIoR}z7pZ24x%|2+51Qx1^B6M|-rL~=K><6NwuM=?p-HZEhMwY`uz{0l z3K0^K-4EWKy4;Z8a%*RJe!*)I2II%_o*#RxVH!@Z8gXHMT&_P=MAfD-Cpz9RTW7Q- zOB0wFtH~q-WDJUiTwjmMXyib}gR9?04RkIR=M#Ac3t?lkBV0pY2J33KOCz2?eY)%) zkWeP6&{!Bn*XOm`l+bV&K`M83lzgj~2)YnV#+hHXk@KV0)5%ebhzX0$(5kD`&`E*O zokZnzx}U^~2cA`6UyZwzD~?u7L2PXR%FTV3Xja2e7anHs;)TwAvNWZ`b3i!gvMaci z639C+Q#)D!D!2dAJy_5y%z%)iG}xru`w6@x#%IK<@17IBUxx4;42>;ud??m?p^I3f zcwy;#@Dq3k9z*eQC%>MOLAeJxsb-{%C+wfLYqR1tE}O8;T5)+kix?r1>b)^CwvNwJiCy$LcWXQ zCuH>(8Id9w<6IMKFnSbg zNmWD6f}YndnY>%v`?dDTBOevX5;|SiMF#a@+!4Ip_RFq%1E;Uk?r4UVw)Ru?g?9K0 zmWto}UA1KpdBK2ZWo4ai)otF0l{m&ZdLh#^uN9nGJEi}6rU%Vqpm89fYK~$u2cd84 zwktUdK-qBPcd6_$(=l$LeF-xwobZ<;XRTDnrwXXfsK>Z5Onza3`a^K=ao8-}o7lSf z7Ysje4z^CTzu@HTDfc)x5{j$8EwBUMqqw%oumdn2;E7wUNL*RXQAJXuIveJPwff<> z>Hmo4(9{&fi!Ptt^jheY-xX<)i#2x=b>ul`SCLfgMfU+#cOIcAy*O^ZfWI0O#UFS- zZYcU2#KTsy2K%aTgCJqdOZRA9hAAb1sd^U)0$NT#?u;gdtDE4~Q~&J#Z6B@x!3i@4 zD2y(N`-%#?Nzvchi8$}PBJ%|reJ4FAml8S(tcui=$dnYD;emU4V#;VzidmfijQc!w zs%G9XQKDnVEsVx7^!__!%)8X`XM`!fgnNsn?zs6qG9*p=ADhVPTB-A9D1 za^?P-DBND$Ws(O2&x#`CV+q%4DdzoskoMqb*f1T$wS)Gu`l(GBx!n;-#_hYUMRJ$W zt0XAZe#AFx2)zqim0>aj%fUhdQXtzg<4(>tDCbVu(W%O>HGK0SPTvbaGK2aKNu$T- zc6G_KzWfk41hbdMTxw;^e7H70Y!H!}uM^Tq%%FNj!GBJ1O--W436Jf`Rfk}DrY^ac zO2D}O%P%$gJ8}m6*@a&iMxEhCVV}FcTN{s_S_+54#-?l%={QA0c+dp}iCH~neAw~3 zDRV2VgiTJ77X-Zg%*Z^CDXxc>m7zu%mvM?e%?x`d(2`R45Vz44)Y1v7ar4l92th;O z!qvHpU;rI)=5r(Gx~8s7BN*sgDSak)Q^Jw9O&0f5g*RcB3zJTS-fX$K6x2Eocc?JO z-RGC9YK1!BBJ@IDxB2es&tD$A{xr>o_nOUF~en-DR8@}|C1ag`i;q&SlLZF6ZtO` z%}}5@hQ>Z1I8^E%ODbx-trwuZrZ* zw%fQp<%0+CpmRB6y;%2`n|wUa>K1kjFi8I!&ek0#XjQ z=MyL~*`YA(7Kui-2M_#qskz~?({HSjAqc{j;jSVo>T!eD0#}0PW*3XIxYC%(!sQs{ z#qp`w9{XH{1^c%gtA4xl@%6sSi^}?2B$|mXe)BFDI()~kwCiNbumUqU7{BmSv@X|x zW6Ih-yAx)zR@wvr0ZiF+*RPyk@`myWB#PFhjuGSyQK{?hh4MEGR|n3yLgA;pQh1hH z_Po!U!N*u;Bu3WeMzpz8-`@V#4e;sE!m;x9j}IMB=+2Vuu)UfsDr$?PKn3(0RV}5P zCbz~@u~Ux+4<4u+a5{DBN35;GC;`lyez!jo@!B*Xb?&Ei)4G`2z8#E9sP6TD&nnrX z=%K@m0#Nh>FvlK(TV3s~)NrAdJY_xKZ|ovPTlFYa<(MVkHBzUgk*O8ubl}LNwL=^E zfwC$ex;(BoC_;wA-X!!+yW-tle-(h{ZibL6-i4-4f*U5kwN;A;Vsub|V4EBpGw#Gl z4gUAgF;dw!O`x|{T9JnJJXR{vXy<^fP03W!*C?Mzha!2V zBaJ-#F}h=BQsOcY5Mfdaky9l-D`phQ0F_5|{}E_eDP#D=-$SzZOnsZ;0)3Ver#%?B z6q!bO4wh?zKFAozmXtb|utQgO$c}R?cKlX@)62ykZ&?Z|;cep&adYckG?0rb+Ack> zTvnp?BgupI#eL@y9{#?)^&o-9$F`as7myZHhWGrVIYiAtX#E5D;3+0!TZxL8Hk4@K z7RKX$sVdQZ2SP{aS9HVdZ~5~rFwOvU-X_wRA5)hS2l2EDrw4g~eC`!}Ki#7$=?J79 z<2!PLSOx%_B--EFPtQ7LA+&*-WB)L80Pd>k;i@A|D-@GAt@*))ZZEgm9rj6z--h>@ z8$AuYsP~Qie6v8sfSwi=fm#QVKNlDbJyM(?F(t{!98+X%NFo7#OH9Q*q2U$WViPbPRs@(prd02yva;wkJP|9H z6AF>C+NIrCGIhhA&x&>(qNmucdqWuIF%v`cOSz_)yDV@hrO*UlqQ|Co-T>d&o%;E9 zM1egRt7Il>g7q5*AqM;VjWl2ThUyUPO^m9<4YLYL>T5gxujI&VxLg>FAV%xsm+9`% z00`VL^5oYKQq?rJCJlmm`@25Nh2d76b@tZs2O{4Pv;vd8)gle3dwY1-cv6 zoi#lg68gE|@K;>N<(>yFp~uLZYomZCv`P?i*G*F!QGPG#g!TXlWC1g>cgantS;0~q zs0SJ4Hjliz>0cqxtZ8NX6QWK2@~Ei$c-WN8)IfN!&PVnH`vo@98|s!r(eVjWPT z-&7x9%93MR{wsC}i;cOZ7Y%+mGK07E_5$M^=_GeCZLMH2hsM>j)-%5o1r!OQ-ET(T zU%383>r2Q3?VPq5+&yQ<5GigC{};i@FtAOgK(B9^FHQ2P zlzeE_8Z-5x)~6}ZB3bBn8xw+2jt0!!&sYt9Q>~X)eRwl*$3C(>H+v^P5l^7k2qdun z4dj4ea2>Zv$r8!m^S7CoMdreMCOrJIpir)l>ZznWpnEq9q~TtZ+L}q)Ty1&l@UA4J zLj4=#RaZ%d8b$Q-R4h}6Jp(32@H|blP4z52Np^f8cJ>?&sVu7(U&w))f7LAN#j6;U z8LZrkvFT**BE8u zP=e*4^51W9il{bUY32y%drBtB_;yM_yb?)K$>Z?jsz2~f401FtLW}h&bMx#zIhb<` z?S6B58zs1zGeLz9bKTYO*347aA}iXdmjPoe>CuK%mE0N<6H^G}JyM;bF!66WZI3#v zeFmKf-(Y?F>R4ISWaG|<^okQT;Jd^6Bd*Q#Z1=$|j5v6b@SbGtaU1J+!3^$L0H-XE zXTFB2o6C=kBaq15dw<-W(m{>>F@FRASeon;n6e8>T59O$)qJnip+y&thf>xb%?uf) zdJQ8bjUMTT9mxVhF~AOm9OT{hsjy`ZF!*71r@LNX;NeiAAQbmF?ey1R!lI}wU+q=++;SyKX6Uy zOPe<@d^b!DH#L=%PWxXfnpy{2*qD6q!jKTP^xEVJ6;rf3P&B>RN}V9QHir^@?^cGJ zr8?50z@Hqj`nv?O&8vlQwG+_-EoBAF%RG`C{sHyK^58t?rxMqvx3zJnxo2MR=Th!C z#L3K~(h@P9#@^Q~dCWbuYHyabD5VUz42qQ=X}i#<`}YOn_kNt?c1Ec%KfSxQDw?%_ z0he+AlFtkZ>aUn={QJ0n{D@a0r;ecXG3yMBEGN}}muZRk6Pn;%?4z0HHn1vhv0=^; zMAa5MxFlRGA=@Q85FbfFTwZ6El9i#^xX;>Vl1TA@_0^d9;!&+MZZL7nm-kXQsX9wQ z^D^$T6?{1lT6uvhbc4*L^dOd8?L66h zXS!Bt%V%{2D<&{I7O#|k{Tb%G1Los|KN9NjTgB6s9oGkwg(2Y`SM496{;F*5HW1`Q<4zBzdrqRAtqY_BHh%G@$Znc`Lk)n!{-mynh2I@T9tC_#mgtRW>Mvv-X(v1Ot z9Yf~_%D=p^z%AoM{xaY}2Gjsk?)1f#$#FB`r>twvM=GdgZ( zy>L8GA&ZM`^CL2_-K(LYxFC@1hr~f9^nJN>&_1A1tT5e{MvWRZHM5GISn0jv=3@5= zm8|2bU8K@vh&;LN`=IPUPO_aZaIi>BXJ`*|?Z;B~J*_TO#uy%Z+MfY+)}uTd4kK|}V$HXwfr zyI^)c$t8OJ3p>)6I$+oZ?-+x8g)8)UQdk}=pN=vu$$ja&QKRWBkf^xiE{sH4TiLu3 zY`wwv-lw|-1^_&FFYW?>1k~kTvzTI%pca_ckbAR7Z7ZP+jdNfq{4@yx41Ia zznb9}-j_x4_J)1jhzuiza<)Z&TBuN8wwg@$chl&E%?RD1|Q7vi%nSlV>w)B5vRwUUJeTBL<9t0k-@O0{fZi+ zdQ46#+rT>=z{JC#hep zD^uUWu<8lUZ+O=^zYOx6L5qH4pWe$|Wu&5j8fK4wWWsny(vky;{VR0JdUh5X zNjl;k-oJ|s7dzX!HI)ivg$p6Iv)k$WKpWJSGUK1x1R2q3D&LA!g)p20#sNf9=f$sx zxf6yA-G&eb2k0MJ+nUZS4E~r9vd`(+ovG)q_)Skme{tltm&AlfIgRIG8`eNpq)_HP z=QA&Q5b%A8=ipyebcSAqddq|?PRrc6_r-D3QDlU@(CELO=K87&;ea7WYyIS&N9ls- z$i{En(U0@n+~{en9PO3qhR%zo;6p_dRrn1Gk+?&-QjP3c9^Ij-E2h?%&>FtWY6!-q7d3A59A(rm@#c(G`vq zw~S97u)^_gMWhMGtlk7xi0?{<^mA;}s2MWXu zpmZ4 z_f0p3BDA!mOm7Lu#v4D-s;U`{6gB%-!ni{_31}86&7HJ!vW zGCr|Or|;0mo`5obSG^WmzRs8`t4cLS<26JfHw*kqe82*P+DIAB3D6C-7FB1fWYxb? zSHI3?rfXcPuz3A)J%vIu(KVFPA#ST1@I-!9cUAbHw91>fLo*3nOOd~V3vg6yJCCeq zd*&azGd0X=4#tpEScmo#BM9%@n?RqABMw48m;9mqdJ6632_`EWuvk0G;7=|8m5`(L zV-7_76(LI+7{qq*fWFUk0J4-!4+k42qIG}hSj*AA=YoUV?JuHg zVIb^JB|{a1`xZvEzFkf^y3ZnC8Jw}?UQ*WWR-JnxEoz2CNn@X@+WPIt6?IaF9yC!K zN#8#Dyw7d43%<~<;>)IOq&GwVF3lK23p{gJr}i;8frfBmaHv~=0rk~>TgOKEd=l}a z`8UB2BJvYGAvD3lMg6ZyIk`^~cX08LJ9D`RAu6jw_GMC`MxI_W5lN1C9>zgoB%L`q zyX(#AzqzMYIPzID`~nVp9hVIIkaT72ApHtB z)P1=>J&C!Z_jYcq5T}CIiBtfk zg$lswMMVAvum_=6w})DSmvyT`GxSWCmvKMRF70AQ8H%l3CA$RdSo^USwsz6R$<~-d zoukFAJ?TjkTV@WN5C6=N00&+xfWk`3TV-!5DIPa{r+F{vc)Rr6wJ##@7^gvsEA+Rw$PtQSHB zH!h{hZre{RN`!`N#>c!8mbDv(!d}u8?!dH01e-wjc$S7Ci*b2<+O&6mEr>p*4Qu&! z6w!Bdl@0e3a$#BHKBbc=1l)xqa`yg1POy+pe8*a(b3@ zS>h%ch0_b~iSQt77?uET!V7FUX3|Ktagzg-nHop>3b&6%Vn<#>LpHJ|UQk^7=1_l= zg@F+L<|Tfg&-Z1Jiz`>+9LFb`a1fO5Vwb+$b5wXibwBE>`#2l;4|ME2NaA!<6#8Sl zuQJYlC8wQR>-ih$OnUFePQtC&70;?jB+yI>BG|>uYQZ5WWGsvE>a4iT%f-K%bg zTl)szMPB#MnIPSy%`AhR{_yOEpxk5M2a!<#Sw9w66!+*95V1;_hxbiHTD*>5At@(S zhd2yL*8qrNA#Sbu9(L*69lP`Omz_@9lrJn%Tq8nKi4Hq53Jv-Q`#~Qy2&pwv&siIC z5hhm1Ig-*CE5n^k34oPh*Q7tJ=Q5}ntDaeh@H*HA&2$V(4=t7xoLUr?iG-c0$LaF+ zk6s6i73k)VEr_ZcWmW8M#oxi2JMx&zL3Y-5J$6JTD1}5YO>|$se*LhrR6;PXA(am$ z^?|bCtG!;w(;pkcVCZ@a4;8|uL+#D7Um85XQ5G_DPKkcgH$_ z-5nzs6RB+Ibks4lL5uDyQSOb{T01g8bqY~FGDJIz>62I&Y(aWxBb`(xU8zmuNGEH* zdc4S%yK6;>4Vjv4nPSQd5b~Liw{xiA!&WTC3L_F4sabT)BVS({E%%*_XqM#jwJxZjw=$>R})&p4ZamIdGqfUt)8_A+)gJbmiq76EkQRtojR@c*j za(*X2e*2CEcstd2Qh+n^Fv%{^l~P7~KdukpLXR{qkSy!7j|s%%PA=nmrgHsVTwm|K zV^_tc=0{^l`Qj6==mi!sC#P1^2s5n|2g_BwA!zE9PyNha$N<)c`Lj!x2jvq9SnYX= z5tN#^1jsf08zyo@ayT-xNNv-Kl#g=zj(Z@%hA0BLW$ho|J$bJQoa{4UnGw!Rse>U>8cLw-Hcb!{<{i?7Rdv_7{ zmjs)t{lQ_fOmXY%*)9OaH^=m1t&0LWdE`{uQLuUWu4rLZj)D`$dG$WAMMANfeS0pjr#Eg3n;!CXj)^8>SFb*X zmuiA(6ZDAC^7$Vfsc z%vf+`KZRBtg!=UU_vUge8{)Odg}nRmW*q0LYl8d6YdUNdIrQ^|oaDncjX8HNtXli4 z;+o5VFth6rG|PW-9NxUy_3>0Ic;@Cz&%>zizvV~RjliKdll!2b(kl|gFb^%ulv=K- zy(NL6C??yHo)Axdp)O=vM9sJQ@eT~qKQ?}9`C%cIpr&^HSO~BP-MI2TsEzL)YAmL4 zt%zxz_C-gCMm`0g`>K4+=Xsb&&d_Myu=U<{^4T6INhBuAlc#}`*VGn(D)bm1X;)o$ zMNsMp^_e7~6$}E}2YTKz&HQ7aNjsI}RAbK(d?)<}3>bXMu*SG%YVpdit-p7Bh})0d z_TO0Rjif@{yqJ2vlO0A=4f3Hzgmrw-oD$#PwnHWl;HP2cB$@FS>4=jXpf2h98KZ9I zdos)SeI9`aKbSo2u4YX)ESJ}2KlKk4WW>b^G(Q%Spu7P24^{*Tum<@VOcwaot&J_OE#L)m(avIsH1OGdtZZXjFt z6n;t;W|HVQ5z}$C17;u{HBYv4Q}E(b#BblvTBYC9%Evhh7!Y&gM0-dv#2{mTy6N?h z=|1`&gaBbg=o2c;%N}@umGRfIc&`4+@tayYpW|p15Q=%?!}=ZdG~Cq1CBEac5OX#x zoOCliF3FzC#P1yncK1uF>F}m!z$SgtQ6c@Uhgq@Qm?Uy&+KxYGpDNzbLS-Itrvqlj zB=X&L5p(sR@noZr3@L`5XT#93ed=HxtguS?Lr=QQxIOUH8hdK(zHMm6T6*ukWpYX~ zkpWiv;MrO-`pmpeBZ2$Yf@-B>5066(a&EKz(Sm4Pjm~)*iXKQPwb}b=lf4)nPH+NU z$hS)NZ(ODVt^)?-JK@LqQOHt_hgqNvhwjYG48QQ7spsON_N?Y>mqCQzWF&&`E}aR7 z7C5cUKmdj;GM?7I-lLr`j!|;#=g^va;~M<&e=kDi>FRxgu8_Fl*Nfv{!IRx|T6#^` z>`xJI1std(-nlrqlpN%LKRK6HVtvO^1QfD9W;f_EjFKcp>NRhzipo5#;ZSuNIHrFf z*-=FY7ZS?O4j8PR)*DBhy_2MYJ`T7J;w-F+CCiG=#cC|S({`~8xI7gwGJnur92rG$ z0wXV*I5<5g>>6M5&(&ByGn;5n8#(k81CD8ATgL;8yI>sEs#?7wH#8ta%V5(RE*++v zrB*4!Nh#^XtLBCG!UcB=2U8!ZnB$j=O^f(7@@`$3H*X+p6X@lMUydL3kAJ;0CPqXF=x*yw(exAY;_U6_=P3g?u z)~z4A4Vq8KvfITwI2##iU-l3$UyV>^7^C3eHEvFL4_U=%gd{H}n3&rrp`?mL03qY@gDsG4%=zMB!2kLkiv8Hrhh$2kGVP z)wBD!2%BFoT;H<{H*0;>$ls;Z7GM9;@s@9I@Y{*e5-t1vwL6sd36=pYztC^sb=R(X zGZiv0{y0>uRME1>iO^eDfkrD zaOOpbL(2#0caNQ(ci3NV=tS71y>A4~tS4|6@~38Qj^@IzEys@J6ll3OgNh(uH9UR< zW)Z5~0klo6M6P6pB<}93=N*DK$!?xLNL+;2&s;HaRI-n@yh(w0_z%;*S8H0L61K53 z`N*Z|viG^|*_+d^J!z59@SlI5Yg9nUNy}Z!ntq}CwleT=qe(%A_~X}?aANA^cbCk+ zwycw^!){_ODoUIm`R0wZs&e#IGhjxB!t}lGH(mBRw{^aAZKE$ZsjzbfvX+r! zJ#Ua98E2O*spd7;ZR#EV+Y}}QS2FV|df;k_arLWw?>wgb^jqiakQC>hc?1*! zexDyxNQkYWGX7of5T&xh`iOELvObabi_my0L{Xfa4RS3}2D5|sHQ3>K^DU0aFql0mEbsC7h_y7fvSIHJgXP}B? zm~lxUpRvv0^IFAWO=2=vMLS@attyp7%r*b#r0;gS$b^7ZALb&Cw`R?K`~+=+Lmt@2dh<4bTfAi z9LOk|!W+Br5tOg{clwcf+>*4LC&cU5>jAKlKEadJ5S3gcj*B-KSDzFZje76cWPGmu z$v*Yd2H_OD>Fhw}N#+-r8=f~5=ipK^Nam;Nt$3xQeSt%KWUXiafx|p|1KNd>#vm%2 zWu_S{AIUwFUl|ru;MHV8|2*OY8O3vM18E&kiQIDn%fzwJd}8Qxu=1AE#9)r!xx4Gc zpoI7N)|HWi4%@M7xoR%X*P2!CV?48Z6v))ngqz9aNyhd^%VcU-jxo_%xYF_)7*0@| zkc+HTl2J&?(jaJeL)9t?E z^W#|#(T`?(lX^+mD?)!Hjc;NQ6_$re7K4rOI(>y{B5efXo>SH>ru}AGM=NP_kiG^; zQGfIG?U!M1h5|G|zeM&omyVMwRvX(yK^b4KlfPnFSVtv)QKt!cNzpp_OGBA_l+EX{ z-0$1;njhFhzwchPhsAthP+@(ggian3HJ^-gq0Sfo)GM@-#T)tQ3e#}A6T9BSq4mmY z5HeD|G3Og0?2^)BIf$lmEVRtw^@-X#!j9!g?hlc1%XjGSr9FCK(w9tu``Y?i<(dxR F{}0#CmxurW literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-xxxhdpi/ic_shortcut_compose.png b/app/src/blue/res/mipmap-xxxhdpi/ic_shortcut_compose.png new file mode 100644 index 0000000000000000000000000000000000000000..21c3a4c058c22ee4087376df270268f089af469c GIT binary patch literal 10507 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 + + + + diff --git a/app/src/green/res/mipmap-hdpi/ic_launcher.png b/app/src/green/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fa827e9a54dfcfab91b4ff4f099e0870cb5a1492 GIT binary patch literal 5389 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 literal 0 HcmV?d00001 diff --git a/app/src/green/res/mipmap-mdpi/ic_launcher.png b/app/src/green/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8c0b5d103416d53a07bf6e1efaf31a0301d91ebe GIT binary patch literal 2865 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 P00000NkvXXu0mjfofn$u literal 0 HcmV?d00001 diff --git a/app/src/green/res/mipmap-xhdpi/ic_launcher.png b/app/src/green/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a4bdd1fce3aff065c6a573aa43fcb0108322f188 GIT binary patch literal 6540 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 literal 0 HcmV?d00001 diff --git a/app/src/green/res/mipmap-xxhdpi/ic_launcher.png b/app/src/green/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fa47fdfaedb08ddfa9aea88a2e7223fa7de9c997 GIT binary patch literal 12355 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≧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 literal 0 HcmV?d00001 diff --git a/app/src/green/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/green/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2169fe1f40d86030d8ec550c258df5386c3ce809 GIT binary patch literal 15128 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 literal 0 HcmV?d00001 diff --git a/app/src/husky/res/values-ar/husky_generated.xml b/app/src/husky/res/values-ar/husky_generated.xml new file mode 100644 index 0000000..1c8b87c --- /dev/null +++ b/app/src/husky/res/values-ar/husky_generated.xml @@ -0,0 +1,56 @@ + + + توسكي %s + + + Tuksy برنامج حر و مفتوح المصدر. مطور تحت رخصة GNU General Public License Version 3. يمكنكم الإطلاع على الرخصة على : https://www.gnu.org/licenses/gpl-3.0.en.html + + + الملف الشخصي لتوسكي + + + إعادة تشغيل توسكي مطلوبة قصد تفعيل التعديلات + + + يحتوي توسكي على شيفرة وأوصول صادرة مِن المشاريع المفتوحة التالية : + + + مدعوم بِـ Husky + + + + + + موقع المشروع :\n + https://huskyapp.dev + + + + + تقارير الأخطاء و طلبات التحسينات على :\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + الولوج إلى ماستدون + + + إضافة حساب ماستدون جديد + + + تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق. + + + + + بإمكانك إدخال عنوان أي مثيل خادوم ماستدون هنا. على سبيل المثال shitposter.club أو blob.cat أو expired.mentality.rip أوالإطلاع على لاكتشاف المزيد ! +\n +\n إن كنت لا تملك حسابا بإمكانك إدخال اسم مثيل خادوم تريد الانضمام إليه قصد إنشاء حسابك عليه. +\n +\n نعني بمثيل الخادوم المكان الذي استُضِيف فيه حسابك و يمكنك التواصل مع أصدقائك و متابعيك و كأنكم على موقع واحد و ذلك حتى و إن كانت حساباتهم مُستضافة على مثيلات خوادم أخرى. +\n +\n للمزيد مِن التفاصيل إطّلع على joinmastodon.org. + + + diff --git a/app/src/husky/res/values-ar/strings.xml b/app/src/husky/res/values-ar/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/husky/res/values-ar/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/husky/res/values-ber/husky_generated.xml b/app/src/husky/res/values-ber/husky_generated.xml new file mode 100644 index 0000000..c0dcd42 --- /dev/null +++ b/app/src/husky/res/values-ber/husky_generated.xml @@ -0,0 +1,14 @@ + + + + + + + ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ + + + ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ + + + + diff --git a/app/src/husky/res/values-bn-rBD/husky_generated.xml b/app/src/husky/res/values-bn-rBD/husky_generated.xml new file mode 100644 index 0000000..6976ccc --- /dev/null +++ b/app/src/husky/res/values-bn-rBD/husky_generated.xml @@ -0,0 +1,62 @@ + + + টাস্কি নিম্নলিখিত ওপেন সোর্স প্রকল্প থেকে কোড এবং সম্পদ রয়েছে: + + + এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে + + + টাস্কির প্রোফাইল + + + টাস্কি মুক্ত এবং ওপেন সোর্স সফ্টওয়্যার। এটি GNU জেনারেল পাবলিক লাইসেন্স সংস্করণ 3 এর অধীনে লাইসেন্সযুক্ত। আপনি এখানে লাইসেন্স দেখতে পারেন: https://www.gnu.org/licenses/gpl-3.0.en.html + + + টাস্কি %s + + + টাস্কি দ্বারা চালিত + + + + + + প্রকল্প ওয়েবসাইট: +\nhttps://huskyapp.dev + + + + + বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন + + + মাস্টোডনের সঙ্গে লগইন করো + + + মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। + + + + + "কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন shitposter.club, blob.cat, expired.mentality.rip, এবং <a href=\"https://fediverse.network/pleroma?count=peers\"> আরও! </a> +\n +\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n +\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n +\nআরো তথ্য <a href=\"https://joinmastodon.org\"> joinmastodon.org </a> এ পাওয়া যেতে পারে। "more! + \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to + join and create an account there.\n\nAn instance is a single place where your account is + hosted, but you can easily communicate with and follow folks on other instances as though + you were on the same site. + \n\nMore info can be found at joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-bn-rIN/husky_generated.xml b/app/src/husky/res/values-bn-rIN/husky_generated.xml new file mode 100644 index 0000000..dcfd50a --- /dev/null +++ b/app/src/husky/res/values-bn-rIN/husky_generated.xml @@ -0,0 +1,56 @@ + + + টাস্কি %s + + + টাস্কি মুক্ত এবং ওপেন সোর্স সফ্টওয়্যার। এটি GNU জেনারেল পাবলিক লাইসেন্স সংস্করণ 3 এর অধীনে লাইসেন্সযুক্ত। আপনি এখানে লাইসেন্স দেখতে পারেন: https://www.gnu.org/licenses/gpl-3.0.en.html + + + টাস্কির প্রোফাইল + + + এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে + + + টাস্কি নিম্নলিখিত ওপেন সোর্স প্রকল্প থেকে কোড এবং সম্পদ রয়েছে: + + + টাস্কি দ্বারা চালিত + + + + + + প্রকল্প ওয়েবসাইট: +\nhttps://huskyapp.dev + + + + + বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + মাস্টোডনের সঙ্গে লগইন করো + + + নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন + + + মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। + + + + + কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন shitposter.club, blob.cat, expired.mentality.rip, এবং আরও! +\n +\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n +\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n +\nআরো তথ্য joinmastodon.org এ পাওয়া যেতে পারে। + + + diff --git a/app/src/husky/res/values-ca/husky_generated.xml b/app/src/husky/res/values-ca/husky_generated.xml new file mode 100644 index 0000000..7153457 --- /dev/null +++ b/app/src/husky/res/values-ca/husky_generated.xml @@ -0,0 +1,63 @@ + + + Husky %s + + + Husky és programari gratuït, lliure i de codi obert. + Està llicenciat en els termes de la Llicència Pública General GNU versió 3. + Podeu trobar les llicència aquí: https://www.gnu.org/licenses/gpl-3.0.ca.html + + + Perfil del Husky + + + Has de reiniciar l\'aplicació per tal d\'aplicar aquests canvis + + + Husky conté codi i recursos dels següents projectes: + + + Desenvolupat per Husky + + + + + + + Lloc web del projecte:\n + https://huskyapp.dev + + + + + + + Informes d\'errors i peticions de funcionalitats:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Inicia sessió amb Pleroma + + + Afegir un compte de Pleromat + + + L\'interval mínim de planificació a Pleroma és de 5 minuts. + + + + + Aquí pots introduir l\'adreça o domini de qualsevol instància, + com ara mastodont.cat, shitposter.club, blob.cat o + molts més! + \n\nSi encara no tens cap copte, pots introduir el nom de la instància on t\'agradaria + unir-te i crear un compte allà.\n\nUna instànica és un únic lloc on el teu compte s\'hostatja + , però pots comunicar-te fàcilment i seguir amics d\'altres instàncies com si fossiu en el mateix lloc. + \n\nTens més informació a joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-ca/strings.xml b/app/src/husky/res/values-ca/strings.xml new file mode 100644 index 0000000..d99794f --- /dev/null +++ b/app/src/husky/res/values-ca/strings.xml @@ -0,0 +1,29 @@ + + + + Respondre a + + Reaccionar + Suprimir reacció + Qui va reaccionar + Activa %s + Desactiva %s + + %s va reaccionar amb + + Nom de l\'aplicació + Pàgina web de l\'aplicació + + Administrador/a + Moderador/a + + L\'arxiu és massa gran + + %s va reaccionar amb %s a la teva publicació + Reaccions + Notificacions sobre noves reaccions + + Sintaxi de format per defecte(si l\'instancia és compatible) + els usuaris poden reaccionar a les meves publicacions + Ocultar usuaris silenciats + diff --git a/app/src/husky/res/values-ckb/husky_generated.xml b/app/src/husky/res/values-ckb/husky_generated.xml new file mode 100644 index 0000000..9c7dda1 --- /dev/null +++ b/app/src/husky/res/values-ckb/husky_generated.xml @@ -0,0 +1,56 @@ + + + تاسکی کۆد و سەرمایەکانی تێدایە لەم پڕۆژە کراوەی سەرچاوە: + + + تۆ پێویستە توسکی دەستپێبکەیتەوە بۆ ئەوەی ئەم گۆڕانکاریانە جێبەجێ بکەیت + + + پرۆفایلی تاسکی + + + توسکی سۆفتوێری ئازاد و سەرچاوەی کراوەیە مۆڵەتدراوە بە پێ نامەی گشتی GNU Public Version 3. دەتوانیت لێرە مۆڵەتەکە نیشان بدەی: https://www.gnu.org/licenses/gpl-3.0.en.html + + + لەلایەن تاسکیەوە دەست کراوە بە + + + توسکی %s + + + + + + وێبسایتی پڕۆژە: +\nhttps://huskyapp.dev + + + + + ڕاپۆرتەکانی هەڵەکان و داواکاریەکانی تایبەتمەندی: +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + چوونەژوورەوە لەگەڵ ماستۆدۆن + + + ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک. + + + زیادکردنی ئەژمێری ماتۆدۆنی نوێ + + + + + ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک فرەتر! +\n +\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. +\n +\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. +\n +\nزانیاری زیاتر دەتوانرێت بدۆزرێتەوە لە joinmastodon.org. + + + diff --git a/app/src/husky/res/values-cs/husky_generated.xml b/app/src/husky/res/values-cs/husky_generated.xml new file mode 100644 index 0000000..3919c36 --- /dev/null +++ b/app/src/husky/res/values-cs/husky_generated.xml @@ -0,0 +1,62 @@ + + + Husky %s + + + Husky je svobodný a otevřený software. + Je dostupný pod licencí GNU General Public License, verze 3. + Licenci můžete zobrazit zde: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Profil aplikace Husky + + + Pro použití těchto změn musíte restartovat aplikaci Husky + + + Husky obsahuje kód a zdroje z následujících otevřených projektů: + + + Powered by Husky + + + + + + Webová stránka projektu:\n + https://huskyapp.dev + + + + + + Hlášení chyb a návrhy na nové vlastnosti:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Přihlásit účtem Pleroma + + + Přidat nový účet Pleroma + + + Pleroma neumožňuje pracovat s intervalem menším než 5 minut. + + + + + Sem může být zadána adresa či doména jakéhokoliv + serveru, například shitposter.club, blob.cat, expired.mentality.rip + a další! + \n\nPokud ještě nemáte účet, můžete zadat název instance, ke které se chcete + připojit, a vytvořit si tam účet.\n\nServer je jedno místo, kde je hostován váš + účet, můžete však jednoduše komunikovat a sledovat lidi na jiných serverech, jako by + byli na stejné stránce. + \n\nDalší informace mohou být nalezeny na stránce joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-cy/husky_generated.xml b/app/src/husky/res/values-cy/husky_generated.xml new file mode 100644 index 0000000..db161f8 --- /dev/null +++ b/app/src/husky/res/values-cy/husky_generated.xml @@ -0,0 +1,53 @@ + + + Mae Husky yn feddalwedd ffynhonnell agored barn rydd. + Fe\'i trwyddedir dan Drwydded Gyhoeddus Gyffredinol GNU Fersiwn 3. + Gallwch weld y drwydded yma: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Proffil Husky + + + Bydd angen ailddechrau Husky i roi\'r newidiadau ar waith + + + Mae gan Husky god ac asedau o\'r prosiectau ffynhonnell agored canlynol: + + + + + + Gwefan y prosiect:\n + https://huskyapp.dev + + + + + + Adrodd byg & ceisiadau nodwedd:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Mewngofnodi â Pleroma + + + Ychwanegu cyfrif Pleroma newydd + + + + + Gallwch nodi cyfeiriad neu barth unrhyw achos + yma, fel shitposter.club, twt.cymru, expired.mentality.rip, a + mwy! + \n\n Os nad oes gennych gyfrif, gallwch nodi enw\'r achos yr hoffech ymuno + Ag ef a chreu cyfrif yno.\n\nAchos yw un lle yn lle mae\'ch cyfrif wedi\'i + gynnal, ond gallwch yn hawdd gyfathrebu â phobl a\'u dilyn ar achosion eraill fel petasech chi + ar yr un safle. + \n\nRhagor o wybodaeth yn joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-de/husky_generated.xml b/app/src/husky/res/values-de/husky_generated.xml new file mode 100644 index 0000000..31f7b3f --- /dev/null +++ b/app/src/husky/res/values-de/husky_generated.xml @@ -0,0 +1,59 @@ + + + Husky 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 + + + Huskys Profil + + + Du musst Husky neustarten um die Änderungen anzuwenden + + + Husky enthält Code und Inhalte von den folgenden Open-Source-Projekten: + + + test %s + + + Angetrieben durch Husky + + + + + + Website des Projekts: +\n https://huskyapp.dev + + + + + Fehlermeldungen & Verbesserungsvorschläge:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Anmelden mit Pleroma + + + Neues Pleroma-Konto hinzufügen + + + Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen. + + + + + Die Adresse einer Instanz oder Domain kann + hier eingegeben werden, wie z.B. shitposter.club, blob.cat, expired.mentality.rip, und + mehr! + \n\nWenn du bis jetzt kein Konto hast, kannst du hier den Namen einer Instanz eingeben + und dort ein Konto einrichten.\n\nEine Instanz ist ein einziger Ort, wo dein Konto + gehostet ist, aber du kannst dennoch mit anderen Leuten reden und mit ihnen interagieren, als + wärt ihr alle auf einer Webseite. + \n\nWeitere Informationen gibt es auf joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-de/strings.xml b/app/src/husky/res/values-de/strings.xml new file mode 100644 index 0000000..8cfaca5 --- /dev/null +++ b/app/src/husky/res/values-de/strings.xml @@ -0,0 +1,69 @@ + + + %s deaktivieren + Reagieren + Reaktion entfernen + Wer hat reagiert + %s-Reaktionen von + Programmname + Administrator + Dateigröße über dem Limit der Instanz + %s aktivieren + Programmwebseite + Moderator + %s hat mit %s auf deinen Post reagiert + Emojireaktionen + Benachrictigungen über neue Emojireaktionen + Standardformatierungssyntax (wenn von der Instanz unterstützt) + Ignorierte Nutzer verstecken + Reaktionen auf meine Nachrichten + Antwort auf + Sticker + Große eigene Emoji aktivieren + Experimentelle Pleroma-FE Sticker aktiveren (wenn verfügbar) + POSTEN + POSTEN! + Beitragssichtbarkeit + Wiederholen + Widerholungen verbergen + Wiederholungen anzeigen + Wiederholungsauthor anzeigen + Wiederholungen anzeigen + Beitrag planen + Es passierte ein Fehler bei dem Empfang des Stickers + Beitrags-URL teilen mit… + Beitrag teilen mit… + Beitrag wird gesendet… + Fehler beim Senden des Beitrages + Beiträge werden gesendet + Eine Kopier des Beitrages wurde in den Entwurfen gesichert + Inhalt des Beitrages teilen + Wiederholung entfernen + Geplante Beiträe + Löschen und Beitrag neu verfassen\? + Benachrichtigungen, wenn deine Beiträge wiederholt werden + %s wiederholte + Beitrag verfassen + Wiederholung entfernen + Beitrag öffnen + Für originale Audienz wiederholen + Wiederholt + Diesen Beitrag löschen\? + Fehler beim Senden des Beitrages. + %s hat deinen Beitrag wiederholt + %s hat deinen Beitrag favorisiert + Wiederholt + Benachrichigungen, wenn deine Beiträge favorisiert wurden + Bestätigung vor dem Löschen anzeigen + Meine Beiträge wurden wiederholt + Wiederholungen anzeigen + Beiträge mit sensiblen Inhalten immer anzeigen + + %s wiederholt + %s wiederholten + + Link zum Beitrag teilen + Geplante Beiträge + Wiederholt von + Beitrag + \ No newline at end of file diff --git a/app/src/husky/res/values-en-rAU/husky_generated.xml b/app/src/husky/res/values-en-rAU/husky_generated.xml new file mode 100644 index 0000000..0b7834d --- /dev/null +++ b/app/src/husky/res/values-en-rAU/husky_generated.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/husky/res/values-en-rGB/husky_generated.xml b/app/src/husky/res/values-en-rGB/husky_generated.xml new file mode 100644 index 0000000..0b7834d --- /dev/null +++ b/app/src/husky/res/values-en-rGB/husky_generated.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/husky/res/values-en-rGB/strings.xml b/app/src/husky/res/values-en-rGB/strings.xml new file mode 100644 index 0000000..5a76942 --- /dev/null +++ b/app/src/husky/res/values-en-rGB/strings.xml @@ -0,0 +1,6 @@ + + + %s favourited your post + Scheduled posts + Reply to + \ No newline at end of file diff --git a/app/src/husky/res/values-eo/husky_generated.xml b/app/src/husky/res/values-eo/husky_generated.xml new file mode 100644 index 0000000..712266d --- /dev/null +++ b/app/src/husky/res/values-eo/husky_generated.xml @@ -0,0 +1,58 @@ + + + Husky %s + + + Husky estas libera kaj malfermitkoda programo. + Ĝi estas publikigita laŭ la permesilo «GNU General Public License Version 3». + Vi povas vidi la permesilon ĉi tie: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Profilo de Husky + + + Vi devos restartigi Husky por apliki ĉi tiujn ŝanĝojn + + + Husky enhavas kodon kaj risurcojn el la sekvantaj malfermitkodaj projetkoj: + + + Funkciigita de Husky + + + + + + Paĝaro de projekto:\n + https://huskyapp.dev + + + + + + Raportoj de cimo kaj petoj de funkcio:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Ensaluti al Pleroma + + + Aldoni novan Pleroma konton + + + Pleroma havas minimuman intervalon de planado de 5 minutoj. + + + + + La adreso aŭ domajno de iu ajn nodo povas esti enmetitaĉi tie, kiel shitposter.club, blob.cat, expired.mentality.rip, kaj + pli! + \n\nSe vi ne ankoraŭ havas konton, vi povas enmeti la nomon de la nodo ke vi volas aliĝi kaj krei konton tie.\n\nNodo estas unika loko kie via konto estas gastigita, sed vi povas facile komuniki kun kaj sekvi homojn ĉe aliaj nodoj kiel vi estus ĉe la sama retejo. + \n\nPliaj informoj troviĝas ĉe joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-es/husky_generated.xml b/app/src/husky/res/values-es/husky_generated.xml new file mode 100644 index 0000000..a43aebd --- /dev/null +++ b/app/src/husky/res/values-es/husky_generated.xml @@ -0,0 +1,62 @@ + + + Husky %s + + + Husky es un software libre y de código abierto. + Está licenciado bajo la licencia \"GNU General Public License Version 3\". + Puedes leer sobre la misma en: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Perfil de Husky + + + Tendrás que reiniciar la aplicación para aplicar estos cambios + + + Husky contiene código y recursos de los siguientes proyectos: + + + Potenciado por Husky + + + + + + Sitio del proyecto:\n + https://huskyapp.dev + + + + + + Reporte de errores y peticiones de características:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Iniciar sesión + + + Añadir cuenta de Pleroma + + + Pleroma tiene un intervalo de programación mínimo de 5 minutos. + + + + + Introduzca aquí dirección o dominio de cualquier instancia, + como shitposter.club, blob.cat, expired.mentality.rip y + más! + \n\nSi todavia no tiene una cuenta, puede indicar el nombre de la instancia a la que quiere + unirse y crear una cuenta allí.\n\nUna instancia es el sitio único donde su cuenta + está alojada, pero puede comunicarse y seguir usuarios de otras instancias como si + estuvieran en la misma. + \n\nPuede consultar más información en joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-es/strings.xml b/app/src/husky/res/values-es/strings.xml new file mode 100644 index 0000000..051b6e9 --- /dev/null +++ b/app/src/husky/res/values-es/strings.xml @@ -0,0 +1,71 @@ + + + Responder a + + Reaccionar + Eliminar la reacción + Quién ha reaccionado + Activar %s + Desactivar %s + %s ha reaccionado con + Nombre de la aplicación + Página web de la aplicación + Administrador/a + + Moderador/a + El archivo es demasiado grande + %s ha reaccionado con %s a tu publicación + Reacciones + Notificaciones acerca de reacciones nuevas + + Sintaxis de formato por defecto(si la instancia los admite) + se pueden enviar reacciones a mis publicaciones + Ocultar a los usuarios silenciados + Se produjo un error al buscar el sticker + Habilitar reacciones experimentales de Pleroma-FE (si está disponible) + ¿Borrar esta publicación\? + Ocultar repeticiones + Repetir para la audencia original + ¿Borrar y editar esta publicación\? + Error al enviar la publicación. + Notificarte cuando tus publicaciones sean marcadas como favoritas + Stickers + Habilitar emojis personalizados más grandes + Visibilidad de las publicaciones + Estados programados + Repetir + Deshacer repeticiones + Mostrar repeticiones + PUBLICADO + Abrir editor de repeticiones + Mostrar repeticiones + Deshacer repetición + Abrir publicación + Crear publicación + Repetido + ¡PUBLICADO! + %s repitió tu publicación + %s le encanto tu publicación + Repeticiones + Notificarte cuando tus publicaciones se repitan + Mostrar mensaje de confimarción antes de repetir + Mis publicaciones están repetidas + Mostrar repeticiones + Siempre expandir publicacines marcadas con avisos de contenido + + %s Repetir + %s Repeticiónes + + Compartir la URL de la publicación con… + Compartir publicación con. . . + Enviando publicación… + Enviando publicaciones + Una copia de la publicación ha sido guardada en tus borradores + Compartir el contenido de la publicación + Compartir link para publicar + %s repitido + Repetido por + Publicar + Ha habido un error al enviar la publicación + Publicaciones programadas + \ No newline at end of file diff --git a/app/src/husky/res/values-eu/husky_generated.xml b/app/src/husky/res/values-eu/husky_generated.xml new file mode 100644 index 0000000..8902eac --- /dev/null +++ b/app/src/husky/res/values-eu/husky_generated.xml @@ -0,0 +1,59 @@ + + + Husky software libre eta kode askekoa da. + \"GNU General Public License Version 3\" lizentziapean zabaldua. + Lizentzia hontaz gehiago irakurtzeko: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Huskyren profila + + + Aplikazioa berrabiarazi beharko duzu aldaketa ezartzeko + + + Husky-k ondorengo proiektuetako kode eta baliabideak ditu: + + + Husky %s + + + Husky-k sustatuta + + + + + + Proiektuaren gunea:\n + https://huskyapp.dev + + + + + + Akatsen berri-emateak eta hobekuntza-eskariak: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Saioa hasi + + + Pleroma kontua gehitu + + + Pleromaek gutxienez 5 minutuko programazio-tartea du. + + + + + Sartu hemen helbidea edo mastodon.eus, mastodon.jalgi.eus, shitposter.club bezalako edozein instantzia, +\n +\n Oraindik ez baduzu konturik, instantziaren izena sartu eta bertan kontua sortu dezakezu. +\n +\nInstantzia zure kontua dagoen gunea da, baino beste instantzietako erabiltzaileak zurean egongo balira bezala jarraitu ditzakezu. +\n +\nInformazio gehiago joinmastodon.org helbidean topatuko duzu. + + + diff --git a/app/src/husky/res/values-fa/husky_generated.xml b/app/src/husky/res/values-fa/husky_generated.xml new file mode 100644 index 0000000..815fc5f --- /dev/null +++ b/app/src/husky/res/values-fa/husky_generated.xml @@ -0,0 +1,56 @@ + + + تاسکی نرم‌افزاری آزاد است که تحت نگارش ۳ از پروانهٔ جامع همگانی گنو منتشر شده است. پروانه را می‌توانید از این‌جا ببینید: https://www.gnu.org/licenses/gpl-3.0.en.html + + + نمایهٔ تاسکی + + + برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید + + + تاسکی کد و دارایی‌هایی از پروژه‌های نرم‌افزار آزاد زیر دارد: + + + تاسکی %s + + + قدرت‌گرفته از تاسکی + + + + + + پایگاه وب پروژه : +\n https://huskyapp.dev + + + + + گزارش مشکلات و درخواست ویژگی‌ها: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + ورود با ماستودون + + + افزودن حساب ماستودون جدید + + + ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد. + + + + + نشانی یا دامنهٔ هر نمونه‌ای می‌تواند وارد شود، مثل shitposter.club, blob.cat, expired.mentality.rip, و بیش‌تر!. +\n +\n اگر هنوز حسابی ندارید، می‌توانید نام نمونه مورد نظر را وارد کرده و در آن حسابی بسازید. +\n +\n نمونه، جاییست که حسابتان رویش میزبانی می‌شود، ولی به راحتی می‌توانید با دیگر افراد روی نمونه‌های دیگر ارتباط داشته و دنبالشان کنید؛ انگار که روی یک پایگاه باشید. +\n +\nاطّلاعات بیش‌تر می‌تواند در joinmastodon.org پیدا شود. + + + diff --git a/app/src/husky/res/values-fr/husky_generated.xml b/app/src/husky/res/values-fr/husky_generated.xml new file mode 100644 index 0000000..be1e418 --- /dev/null +++ b/app/src/husky/res/values-fr/husky_generated.xml @@ -0,0 +1,62 @@ + + + Husky %s + + + Husky est une application libre et open source. + Elle est publiée sous licence publique générale GNU version 3. + Vous pouvez consulter la licence ici : https://www.gnu.org/licenses/gpl-3.0.fr.html + + + Profil de Husky + + + Vous devrez redémarrer Husky pour appliquer ces modifications + + + Husky contient du code et des ressources issus des projets open source suivants : + + + Propulsé par Husky + + + + + + Site du projet :\n + https://huskyapp.dev + + + + + + Rapports d’anomalies & demandes de fonctionnalités :\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Se connecter à Pleroma + + + Ajouter un nouveau compte Pleroma + + + L’intervalle minimum de planification sur Pleroma est de 5 minutes. + + + + + Indiquer ici l’adresse ou le domaine d’une instance, + comme shitposter.club, blob.cat, expired.mentality.rip, + et bien d’autres encore (en anglais) ! + \n\nSi vous ne disposez d’aucun compte, vous pouvez renseigner le nom de l’instance que vous souhaitez rejoindre + et y créer un compte.\n\nUne instance est l’endroit où votre compte est + hébergé, mais vous pouvez facilement suivre des personnes d’autres instances et communiquer avec elles + comme si vous étiez sur le même site. + \n\nPour plus d’informations, consultez joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-fr/strings.xml b/app/src/husky/res/values-fr/strings.xml new file mode 100644 index 0000000..fd039a1 --- /dev/null +++ b/app/src/husky/res/values-fr/strings.xml @@ -0,0 +1,21 @@ + + + %s a réagi avec %s à votre message + Notification pour les reactions par emojis + %s a réagi avec + Site internet de l\'application + mes messages peuvent recevoir des réactions + Répondre à + Réagir + Supprimer la réaction + Réaction de + Cacher totalement les utilisateurs et utilisatrices masqués + Modérateur•rice + Nom de l\'appliction + Réactions + La taille du fichier dépasse la limite de l\'instance + Activer %s + Désactiver %s + Administrateur•rice + Syntaxe de formatage par défaut (si supportée par l\'instance) + diff --git a/app/src/husky/res/values-ga/husky_generated.xml b/app/src/husky/res/values-ga/husky_generated.xml new file mode 100644 index 0000000..fb3cef1 --- /dev/null +++ b/app/src/husky/res/values-ga/husky_generated.xml @@ -0,0 +1,56 @@ + + + Próifíl Husky + + + Is bogearraí foinse oscailte agus saor in aisce é Husky. Tá sé ceadúnaithe faoi Leagan 3. Ceadúnas Poiblí Ginearálta GNU 3. Is féidir leat an ceadúnas a fheiceáil anseo: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Cumhachtaithe ag Husky + + + Husky %s + + + Beidh ort Husky a atosú chun na hathruithe seo a chur i bhfeidhm + + + Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Husky: + + + + + + Suíomh Gréasáin an tionscadail: +\n https://huskyapp.dev + + + + + Tuarascálacha ar fhabhtanna & iarratais ar ghnéithe: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Logáil isteach le Pleroma + + + Cuir Cuntas Pleroma nua leis + + + Tá eatramh sceidealaithe íosta 5 nóiméad ag Pleroma. + + + + + Is féidir seoladh nó fearann aon cháis a iontráil anseo, mar shampla shitposter.club, blob.cat, expired.mentality.rip, agus níos mó! +\n +\nMura bhfuil cuntas agat fós, is féidir leat ainm an cháis ar mhaith leat a bheith páirteach ann agus cuntas a chruthú ann. +\n +\nIs áit amháin é sampla ina ndéantar do chuntas a óstáil, ach is féidir leat cumarsáid a dhéanamh go héasca le daoine eile agus iad a leanúint ar chásanna eile mar a bheadh tú ar an suíomh céanna. +\n +\nIs féidir tuilleadh faisnéise a fháil ag joinmastodon.org . + + + diff --git a/app/src/husky/res/values-gd/husky_generated.xml b/app/src/husky/res/values-gd/husky_generated.xml new file mode 100644 index 0000000..ea59b77 --- /dev/null +++ b/app/src/husky/res/values-gd/husky_generated.xml @@ -0,0 +1,11 @@ + + + + + + + Clàraich a-steach le Pleroma + + + + diff --git a/app/src/husky/res/values-hi/husky_generated.xml b/app/src/husky/res/values-hi/husky_generated.xml new file mode 100644 index 0000000..a3a54ec --- /dev/null +++ b/app/src/husky/res/values-hi/husky_generated.xml @@ -0,0 +1,46 @@ + + + टस्की में निम्नलिखित ओपन सोर्स परियोजनाओं से कोड और संपत्ति हैं: + + + इन परिवर्तनों को लागू करने के लिए आपको टस्की को पुनः आरंभ करना होगा + + + टस्की की प्रोफाइल + + + टस्की स्वतंत्र और ओपन-सोर्स सॉफ्टवेयर है। यह GNU जनरल पब्लिक लाइसेंस संस्करण 3 के तहत लाइसेंस प्राप्त है। आप लाइसेंस यहां देख सकते हैं: https://www.gnu.org/licenses/gpl-3.0.en.html + + + टस्की द्वारा संचालित + + + टस्की %s + + + + + + परियोजना की वेबसाइट: +\n https://huskyapp.dev + + + + + बग रिपोर्ट और सुविधा अनुरोध: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + हिंदी + + + मास्टोडन का न्यूनतम शेड्यूलिंग अंतराल 5 मिनट है। + + + नया मास्टोडन खाता जोड़ें + + + + diff --git a/app/src/husky/res/values-hi/strings.xml b/app/src/husky/res/values-hi/strings.xml new file mode 100644 index 0000000..19dcb9f --- /dev/null +++ b/app/src/husky/res/values-hi/strings.xml @@ -0,0 +1,7 @@ + + + जवाब दे + किसने प्रतिक्रिया व्यक्त की + प्रतिक्रिया + प्रतिक्रिया निकालें + \ No newline at end of file diff --git a/app/src/husky/res/values-hu/husky_generated.xml b/app/src/husky/res/values-hu/husky_generated.xml new file mode 100644 index 0000000..1b121e0 --- /dev/null +++ b/app/src/husky/res/values-hu/husky_generated.xml @@ -0,0 +1,57 @@ + + + Husky %s + + + Husky profilja + + + A beállítások érvényesítéséhez újra kell indítani a Husky-t + + + A Husky a következő nyílt forráskódú projektekből tartalmaz programkódot és más elemeket: + + + Husky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthetsz meg: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky által hajtva + + + + + + Projekt honlapja:\n + https://huskyapp.dev + + + + + + Hibajelentés & új funkciók igénylése: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Bejelentkezés Pleroma-nal + + + Új Pleroma fiók hozzáadása + + + A Pleromaban a legrövidebb ütemezhető időintervallum 5 perc. + + + + + Bármely példány címét vagy domain nevét beírhatod ide, mint a shitposter.club, az blob.cat, a expired.mentality.rip és mások! +\n +\nHa még nincs fiókod, beírhatod annak a példánynak a címét, amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot. +\n +\nA példány az a hely, ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más példányokon lévő emberekkel, mintha ugyanazon az oldalon lennétek. +\n +\nTöbb információt találhatsz itt: joinmastodon.org. + + + diff --git a/app/src/husky/res/values-is/husky_generated.xml b/app/src/husky/res/values-is/husky_generated.xml new file mode 100644 index 0000000..2ca21d1 --- /dev/null +++ b/app/src/husky/res/values-is/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky %s + + + Keyrir á Husky + + + Husky er frjáls hugbúnaður með opinn grunnkóða. Hann er gefinn út með GNU General Public notkunarleyfi, útgáfu 3. Þú getur skoðað notkunarleyfið hér: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Notandasnið Husky + + + Það þarf að endurræsa Husky til að breytingarnar taki gildi + + + Husky inniheldur kóða og gögn frá eftirfarandi verkefnum með opinn grunnkóða: + + + + + + Vefsvæði verkefnisins: +\n https://huskyapp.dev + + + + + Villutilkynningar og beiðnir um nýja eiginleika: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Skrá inn með Pleroma + + + Bæta við nýjum Pleroma-aðgangi + + + Pleroma er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir. + + + + + Hægt er að setja hér inn vistfang eða lén á hvaða tilviki sem er, svo sem shitposter.club, blob.cat, expired.mentality.rip og fleiri! +\n +\nEf þú ert ekki ennþá með notandaaðgang, geturðu sett inn nafnið á því tilviki sem þú vilt tilheyra og búið til aðgang þar. +\n +\nTilvik er ákveðinn einn vefþjónn þar sem notandaaðgangurinn þinn er hýstur, en eftir sem áður er auðvelt fyrir þig að eiga í samskiptum við fólk og fylgjast með einstaklingum á öðrum tilvikum, rétt eins og þið væruð á sama vefsvæðinu. +\n +\nNánari upplýsingar má finna á joinmastodon.org. + + + diff --git a/app/src/husky/res/values-it/husky_generated.xml b/app/src/husky/res/values-it/husky_generated.xml new file mode 100644 index 0000000..f594ce2 --- /dev/null +++ b/app/src/husky/res/values-it/husky_generated.xml @@ -0,0 +1,58 @@ + + + Husky %s + + + Husky è un programma libero ed open source. + È distribuito con licenza GNU General Public License Version 3. + Puoi leggere la licenza qui: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Profilo di Husky + + + Devi riavviare Husky per applicare queste modifiche + + + Husky contiene codice e risorse dai seguenti progetti open source: + + + Fatto con Husky + + + + + + Sito web del progetto:\n + https://huskyapp.dev + + + + + Segnala problemi & richiedi funzionalità:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + Accedi con Pleroma + + + Aggiungi un nuovo Account Pleroma + + + Pleroma ha un intervallo minimo di programmazione di 5 minuti. + + + + + L\'indirizzo o il dominio di qualsiasi istanza può essere inserito qui, come shitposter.club, blob.cat, expired.mentality.rip, e altro! +\n +\nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account. +\n +\nUn\'istanza è il luogo dove l\'account è custodito, ma puoi facilmente comunicare e seguire gente su altre istanze come se fossero sullo stesso sito. +\n +\nPiù info possono essere trovate su joinmastodon.org. + + + diff --git a/app/src/husky/res/values-ja/husky_generated.xml b/app/src/husky/res/values-ja/husky_generated.xml new file mode 100644 index 0000000..f3abc9d --- /dev/null +++ b/app/src/husky/res/values-ja/husky_generated.xml @@ -0,0 +1,57 @@ + + + Husky %s + + + Huskyは無料のオープンソースソフトウェアです。GNU General Public License Version 3 の下で使用許諾されています。ライセンスはここからご覧いただけます: https://www.gnu.org/licenses/gpl-3.0.ja.html + + + Husky公式アカウント + + + これらの変更を適用するには、Huskyの再起動が必要になります + + + Huskyは、以下のオープンソース プロジェクトからのコードとアセットを含んでいます: + + + + + + プロジェクトのWebサイト(英語):\n + https://huskyapp.dev + + + + + + バグ報告 & 機能リクエスト(英語):\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Pleromaでログイン + + + 新しいPleromaアカウントを追加 + + + Pleromaにおける予約までの最小間隔は5分です。 + + + + + shitposter.club, blob.cat, expired.mentality.ripやその他 のような、あらゆるインスタンスのアドレスやドメインを入力できます。 +\n +\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで そのインスタンスにアカウントを作成できます。 +\n +\nインスタンスはあなたのアカウントが提供される単独の場所ですが、他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。 +\n +\nさらに詳しい情報はjoinmastodon.orgでご覧いただけます。 + + + diff --git a/app/src/husky/res/values-ja/strings.xml b/app/src/husky/res/values-ja/strings.xml new file mode 100644 index 0000000..1b13a23 --- /dev/null +++ b/app/src/husky/res/values-ja/strings.xml @@ -0,0 +1,24 @@ + + + 返信 + 絵文字反応 + 誰が反応したか + 可能にする %s + 無効にする %s + アプリの名前 + アプリのウェブサイト + 管理者 + モデレーター + 反応を削除 + ファイルが大きすぎます + 絵文字反応 + リピートを表示 + %s 誰が反応したか + ミュートされたユーザーを隠す + リピート + リピートを削除 + リピートを隠す + リピートを表示 + リピートを削除 + 削除しますか\? + \ No newline at end of file diff --git a/app/src/husky/res/values-kab/husky_generated.xml b/app/src/husky/res/values-kab/husky_generated.xml new file mode 100644 index 0000000..845690f --- /dev/null +++ b/app/src/husky/res/values-kab/husky_generated.xml @@ -0,0 +1,29 @@ + + + Husky %s + + + Amaɣnu n Husky + + + Yettwamdemmar s Husky + + + + + + Asmel Web n usenfaṛ: +\n https://huskyapp.dev + + + + + + Qqen ɣer Maṣṭudun + + + Rnu yiwen umiḍan amaynut n Maṣṭudun + + + + diff --git a/app/src/husky/res/values-ko/husky_generated.xml b/app/src/husky/res/values-ko/husky_generated.xml new file mode 100644 index 0000000..9ccef41 --- /dev/null +++ b/app/src/husky/res/values-ko/husky_generated.xml @@ -0,0 +1,50 @@ + + + Husky %s + + + Husky는 무료이며 오픈 소스입니다. 이 프로젝트는 GNU General Public License Version 3에 의해 배포됩니다. 이 페이지에서 라이선스 전문(영문)을 열람하실 수 있습니다: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 공식 계정 + + + 변경 사항을 적용하려면 Husky를 재시작해야 합니다 + + + Husky에는 다음 오픈 소스 프로젝트의 요소/코드를 일부 활용하였습니다: + + + + + + 프로젝트 홈페이지: +\n https://huskyapp.dev + + + + + 버그 신고/건의사항: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + 마스토돈에 로그인 + + + 마스토돈 계정을 추가합니다 + + + + + 인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. shitposter.club, blob.cat, expired.mentality.rip 등이 있으며, 그 외에도 더 많은 인스턴스가 당신을 기다리고 있습니다! +\n +\n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다. +\n +\n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다. +\n +\n자세한 사항은 joinmastodon.org을 참조하세요. + + + diff --git a/app/src/husky/res/values-ml/husky_generated.xml b/app/src/husky/res/values-ml/husky_generated.xml new file mode 100644 index 0000000..7d87c0f --- /dev/null +++ b/app/src/husky/res/values-ml/husky_generated.xml @@ -0,0 +1,11 @@ + + + + + + + മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക + + + + diff --git a/app/src/husky/res/values-nb-rNO/strings.xml b/app/src/husky/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000..62f884a --- /dev/null +++ b/app/src/husky/res/values-nb-rNO/strings.xml @@ -0,0 +1,68 @@ + + + Notifikasjoner om nye emoji reaksjoner + Standard formatering syntaks(hvis støttet av instansen) + Utsett publisering + Del med den orginale målgruppen + Fjern reaksjon + Hvem reagerte + Deaktiver %s + Klistremerker + Applikasjons navn + Applikasjons nettside + Admin + An feil oppsto under henting av klistremerke + %s reagerte med %s på innlegget ditt + Aktiver eksperimentelle Pleroma-FE klistremerker(hvis tilgjengelig) + Innleggs synlighet + Åpne innlegg + Lag Innlegg + Slett og rediger dette innlegget\? + Skjul delinger + Del + Fjern deling + Vis delinger + POST + POST! + Vis delinger + Fjern deling + Del innlegg til… + Sender innlegg… + Sender innlegg + En kopi av innlegget er lagret i utkastene dine + Del innhold av innlegg + Del link til innlegg + %s delte + Planlagte innlegg + Delt av + Del innlegg URL til… + Svar på + Feil ved sending av innlegg + Aktiver %s + Moderator + %s reagerte med + Emoji Reaksjoner + Reager + Filen er større enn instansen tillater + postene mine får emoji-reaksjoner + Skjul dempede brukere + Aktiver større tilpassede emoji + Åpne innleggs deler + Delte + Slett dette innlegget\? + Feil ved sending av innlegg. + %s delte innlegget ditt + %s likte innlegget ditt + Delinger + Notifikasjoner når innleggene dine deles + Notifikasjon når innleggene dine favoriseres + mine innlegg blir delt + Vis delinger + Alltid utvid innlegg markert med sensitivt innhold + + %s Deling + % Delinger + + Vis bekreftelses melding før deling av innlegg + Innlegg + \ No newline at end of file diff --git a/app/src/husky/res/values-nl/husky_generated.xml b/app/src/husky/res/values-nl/husky_generated.xml new file mode 100644 index 0000000..daad548 --- /dev/null +++ b/app/src/husky/res/values-nl/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky %s + + + Husky is opensource- en vrije software. De licentie valt onder de GNU Algemene Publieke Licentie versie 3. Je kunt de licentie hier bekijken: https://www.gnu.org/licenses/gpl-3.0.nl.html + + + Husky\'s profiel + + + Je moet Husky herstarten om deze veranderingen te kunnen doorvoeren + + + Husky bevat broncode en onderdelen van de volgende opensourceprojecten: + + + Powered by Husky + + + + + + Projectwebsite:\n + https://huskyapp.dev + + + + + Foutmeldingen & nieuwe functies aanvragen:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + Aanmelden + + + Een nieuw Pleromaaccount toevoegen + + + Om in te plannen moet je in Pleroma een minimum interval van 5 minuten gebruiken. + + + + + Het adres of domein van elke Mastodonserver kan hier worden ingevoerd, zoals shitposter.club, mastodon.nl, octodon.social en nog veel meer! +\n +\nWanneer je nog geen account hebt, kun je de naam van de Mastodonserver waar jij je graag wil registeren invoeren, waarna je daar een account kunt aanmaken. +\n +\nEen Mastodonserver (Engels: instance) is een computerserver waar jouw account zich bevindt (vergelijk het met een e-mailserver). Je kan eenvoudig mensen van andere servers volgen en met ze communiceren, alsof jullie met elkaar op dezelfde website zitten. +\n +\n Meer informatie kun je vinden op joinmastodon.org. + + + diff --git a/app/src/husky/res/values-nn/strings.xml b/app/src/husky/res/values-nn/strings.xml new file mode 100644 index 0000000..89e8bd8 --- /dev/null +++ b/app/src/husky/res/values-nn/strings.xml @@ -0,0 +1,4 @@ + + + Svar på + \ No newline at end of file diff --git a/app/src/husky/res/values-no-rNB/husky_generated.xml b/app/src/husky/res/values-no-rNB/husky_generated.xml new file mode 100644 index 0000000..21182e4 --- /dev/null +++ b/app/src/husky/res/values-no-rNB/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky %s + + + Huskys Mastodon-profil + + + Husky er fri og åpen kildekode. Applikasjonen er lisensiert under GNU General Public License versjon 3. Du kan se lisensen her: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Du må starte Husky på nytt for at endringene skal bli aktive + + + Husky inneholder programkode og elementer fra følgende åpen kildekode-prosjekter: + + + Drevet av Husky + + + + + + Hjemmeside: +\n https://huskyapp.dev + + + + + Rapporter feil og ønsker om funksjonalitet her: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Logg inn med Pleroma + + + Legg til ny Pleroma-konto + + + Pleroma har et minimums planleggingsinterval på 5 minutter. + + + + + more! +\n +\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there. +\n +\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site. +\n +\nMore info can be found at joinmastodon.org. + + + diff --git a/app/src/husky/res/values-oc/husky_generated.xml b/app/src/husky/res/values-oc/husky_generated.xml new file mode 100644 index 0000000..4adbc32 --- /dev/null +++ b/app/src/husky/res/values-oc/husky_generated.xml @@ -0,0 +1,60 @@ + + + Husky es programa gratuït, liure e de còdi dobèrt. + Es publicat jols tèrms de la licéncia publica generala GNU version 3. + Podeu trobar les llicència aquí: https://www.gnu.org/licenses/gpl-3.0.ca.html + + + Perfil de Husky + + + Vos caldrà reaviar Husky per aplicar aquestes cambiaments + + + Husky content de còdis e compausants dels projèctes liures seguents : + + + Husky %s + + + Propulsat per Husky + + + + + + Site web del projècte :\n + https://huskyapp.dev + + + + + + Rapòrts d\'errors e demandas de foncionalitats :\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Començar la session amb Pleroma + + + Apondre un nòu compte Pleroma + + + L’interval minimum de planificacion sus Pleroma e de 5 minutas. + + + + + Aquí podètz picar l\'adreça o domini de quina que siá instància, + coma mastodont.cat, shitposter.club, blob.cat o + fòrca mai ! + \n\nSi encara non avètz cap de compte, podètz picar lo nom de l’instància ont vos agradariá + anar e crear un compte enlà.\n\n + \n\nAvètz mas d’informacins a joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-pa/husky_generated.xml b/app/src/husky/res/values-pa/husky_generated.xml new file mode 100644 index 0000000..0b7834d --- /dev/null +++ b/app/src/husky/res/values-pa/husky_generated.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/husky/res/values-pl/husky_generated.xml b/app/src/husky/res/values-pl/husky_generated.xml new file mode 100644 index 0000000..7c2ded2 --- /dev/null +++ b/app/src/husky/res/values-pl/husky_generated.xml @@ -0,0 +1,58 @@ + + + Husky 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 + + + Profil Husky’ego + + + Musisz uruchomić ponownie Huskyego, aby zastosować zmiany + + + Husky zawiera kod i zasoby następujących projektów open source: + + + Husky %s + + + Napędzane przez Husky + + + + + + Strona projektu:\n + https://huskyapp.dev + + + + + + Zgłoszenia błędów i propozycje funkcji:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Zaloguj się Kontem Pleroma + + + Dodaj nowe Konto Pleroma + + + Pleroma umożliwia wysłanie minimalnie 5 minut od zaplanowania. + + + + + Tutaj można wprowadzić domenę lub adres instancji, np. shitposter.club, blob.cat, expired.mentality.rip, i wiele więcej! +\n +\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrować. +\n +\nInstancja jest miejscem, na którym znajduje się twoje konto, lecz komunikując się z innymi serwerami, działa tak, jakby były jednym portalem. +\n +\nWięcej informacji można znaleźć na joinmastodon.org. + + + diff --git a/app/src/husky/res/values-pl/strings.xml b/app/src/husky/res/values-pl/strings.xml new file mode 100644 index 0000000..bc3d2b3 --- /dev/null +++ b/app/src/husky/res/values-pl/strings.xml @@ -0,0 +1,77 @@ + + + Nazwa aplikacji + Powtórz + %s powtórzył(-a) + Udostępnij odnośnik do postu + Udostępnij zawartość postu + Wystąpił błąd podczas wysyłania postu + + <b>%s</b> powtórzenie + <b>%s</b> powtórzenia + <b>%s</b> powtórzeń + <b>%s</b> powtórzeń + + Zaplanowane posty + Strona aplikacji + Reakcje emoji + Powiadomienia o nowych reakcjach emoji + Naklejki + %s zareagował %s na Twój post + Ukrywaj wyciszonych użytkowników + Zareaguj + Usuń reakcję + Kto zareagował + Zaplanuj post + Widoczność postu + Odpowiedź do + Domyślna składnia formatowania (jeśli jest obsługiwana przez instancję) + Włącz eksperymentalne naklejki Pleroma-FE (jeśli dostępne) + Otwórz post + Usunąć ten post\? + Usunąć i napisać ponownie ten post\? + %s powtórzył Twój post + %s dodał Twój post do ulubionych + Ukryj powtórzenia + Pokaż powtórzenia + Usuń powtórzenie + Pokaż powtórzenia + Powtórzenia + Otwórz konto osoby powtarzającej + Powtórz grupie docelowej autora oryginału + Cofnij powtórzenie + Stwórz post + Powtórzony + Powtórzone przez + Pokazuj powtórzenia + Wysyłanie postu… + Wysyłanie postów + Powiadomienia o podbiciu postów + Powiadomienia o dodaniu postów do ulubionych + Pytaj o potwierdzenie przed powtórzeniem + moje posty zostaną podbite + Udostępnij odnośnik do postu… + Udostępnij post do… + Moderator + Rozmiar pliku przekracza ograniczenia instancji + %s zareagowane przez + Wyłącz %s + Włącz %s + Kopia postu została zapisana jako szkic + Wysyłanie postu nie powiodło się. + Zawsze rozwijaj posty z ostrzeżeniami o zawartości + Zaplanowane posty + Oznacz jako przeczytane + Otwórz w zewnętrznej aplikacji + %s wysłał(-a) Tobie wiadomość + Prywatność + Może nieco zwiększyć zużycie energii + Zdjęcie + Wideo + Audio + Załącznik + Odnośnik + Odpowiedź dla %s + Ty + Inne + \ No newline at end of file diff --git a/app/src/husky/res/values-pt-rBR/husky_generated.xml b/app/src/husky/res/values-pt-rBR/husky_generated.xml new file mode 100644 index 0000000..5ab03ec --- /dev/null +++ b/app/src/husky/res/values-pt-rBR/husky_generated.xml @@ -0,0 +1,58 @@ + + + Husky %s + + + Husky é um software livre e de código aberto. + Ele é licenciado sob a versão 3 da Licença Pública Geral GNU. + Você pode ler a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html + + + Perfil do Husky + + + É necessário reiniciar o aplicativo para aplicar as alterações + + + O Husky contém código e recursos dos seguintes projetos de código aberto: + + + Desenvolvido por Husky + + + + + + Site do projeto:\n + https://huskyapp.dev + + + + + Reporte bugs e solicite funcionalidades: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Entrar com Pleroma + + + Adicionar nova conta Pleroma + + + Pleroma possui um intervalo mínimo de 5 minutos para agendar. + + + + + O domínio de qualquer instância pode ser inserido aqui, como shitposter.club, masto.donte.com.br, colorid.es ou qualquer outro! +\n +\n Se você não tem uma conta ainda, você pode inserir o nome da instância a qual você gostaria de participar e criar uma conta lá. +\n +\n Uma instância é um lugar onde sua conta é hospedada, mas você pode facilmente se comunicar e seguir pessoas de outras instâncias como se vocês estivessem no mesmo site. +\n +\n Mais informações podem ser encontradas em joinmastodon.org. + + + diff --git a/app/src/husky/res/values-pt-rBR/strings.xml b/app/src/husky/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..821bf7d --- /dev/null +++ b/app/src/husky/res/values-pt-rBR/strings.xml @@ -0,0 +1,68 @@ + + + Remover Ação + Nome Do App + Admin + Moderador + %s reagiu com %s no seu post + Reações de emoji + Notificações sobre novas reações de emoji + meus posts foram reagidos com emojis + Esconder usuários silenciados + Ligar Emojis Gigante Customizado + Ativar adesivos experimentais Pleroma-FE (se disponíveis) + Visibilidade do post + Agendar postagem + Remover Repetir + Repetir + Esconder Repetidos + Mostrar Repitidos + Post + Autor de repetição aberto + Mostrar Repetidos + Repetir para audiência original + Responder Para + Reagir + Quem Reagiu + Ativar %s + Desativar %s + Figurinhas + %s Reagido por + Site do app + O tamanho do arquivo excede os limites da instância + Ocorreu um erro ao buscar a figurinha + Sintaxe de formatação padrão (se suportada pela instância) + POST! + Remover Repetidos + Abrir Post + Compor Post + Repetido + Deletar esse post\? + Deletar e Refazer esse post\? + Erro ao mandar o post. + %s repetiu seu post + %s marcou seu post como favorito + Repetidos + Notificar quando seus post ficarem repetidos + Notificar quando seus post foram marcado como favorito + Mostrar confirmação de dialogo antes de repetir + meus post são repetidos + Mostrar repetidos + Sempre expanda postagens marcadas com avisos de conteúdo + + <b>%s</b> Repetido + <b>%s</b> Repetidos + + Compartilhar o url post para… + Compartilhar post para… + Enviando post… + Erro ao enviar post + Enviando posts + Uma cópia da postagem foi salva nos seus rascunhos + Compartilhar conteúdo da postagem + Compartilhar link para postar + %s repetido + Posts Agendados + Repetido por + Post + \ No newline at end of file diff --git a/app/src/husky/res/values-pt-rPT/strings.xml b/app/src/husky/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/husky/res/values-pt-rPT/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/husky/res/values-ru/husky_generated.xml b/app/src/husky/res/values-ru/husky_generated.xml new file mode 100644 index 0000000..34c4b3c --- /dev/null +++ b/app/src/husky/res/values-ru/husky_generated.xml @@ -0,0 +1,58 @@ + + + Husky %s + + + Husky – это бесплатное приложение с открытым исходным кодом. + Выпускается по лицензии GNU General Public License Version 3. + Вы можете прочитать текст лицензии по ссылке: https://www.gnu.org/licenses/gpl-3.0.ru.html + + + Профиль Husky + + + Вам нужно перезапустить Husky для применения изменений + + + Husky содержит код и элементы из следующих приложений с открытым исходным кодом: + + + Под управлением Husky + + + + + + + Веб-сайт проекта:\n + https://huskyapp.dev + + + + + + Отчеты об ошибках и пожелания:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + Войти + + + Добавить новый акканут Pleroma + + + Минимальный интервал планирования в Pleroma составляет 5 минут. + + + + + Здесь можно ввести адрес или домен любого узла, например, shitposter.club, blob.cat, expired.mentality.rip и других!\n\nЕсли у вас еще нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт.\n\n + Узел - это то место, где размещен ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте.\n + \n + Чтобы получить больше информации посетите joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-ru/strings.xml b/app/src/husky/res/values-ru/strings.xml new file mode 100644 index 0000000..b191e3e --- /dev/null +++ b/app/src/husky/res/values-ru/strings.xml @@ -0,0 +1,49 @@ + + + Чаты + Пометить как прочитанное + Ответ на + + Добавить реакцию + Удалить реакцию + Реакции + Включить %s + Отключить %s + %s реакции + Название приложения + Вебсайт приложения + Администратор + Модератор + Размер файла превышает лимиты инстанса + %s среагировал с %s на ваш пост + Эмодзи реакции + Уведомления о новых эмодзи реакциях + %s отправил вам сообщение + Сообщения + Уведомления о новых сообщениях + Синтаксис форматирования по умолчанию(если поддерживается) + на мои посты отреагировали + получено новое сообщение + Скрывать заглушенных пользователей + Произошла ошибка при загрузке стикера + ОТПРАВИТЬ! + Стикеры + Отложить пост + Включить эксперементальные стикеры Pleroma-FE (если доступны) + ОТПРАВИТЬ + Повторенно + Удалить запись\? + Скрыть повторения + Включить большие пользовательские эмодзи + Открыть запись + Повторить + Показать повторения + Повторить в оригинальной версии + Показать повторения + Отменить повторение + Записи по расписанию + Удалить повторение + Сделать запись + Ссылка + diff --git a/app/src/husky/res/values-sa/husky_generated.xml b/app/src/husky/res/values-sa/husky_generated.xml new file mode 100644 index 0000000..9de7276 --- /dev/null +++ b/app/src/husky/res/values-sa/husky_generated.xml @@ -0,0 +1,56 @@ + + + टस्कीवर्यस्य व्यक्तिगतविवरणम् + + + टस्कीत्यनावृतस्रोतो निःशुल्कतन्त्रांशः। GNU General Public License Version 3 इत्यनेनाऽनुज्ञापितः। अत्राऽनुज्ञापत्रं द्रष्टुं शक्यते:-https://www.gnu.org/licenses/gpl-3.0.en.html + + + टस्कीत्यनेनाऽऽश्रितः + + + टस्की %s + + + पुनश्च टस्कीप्रारम्भोऽपेक्षितो वर्तते परिवर्तनानुसरेण चलितुम् + + + टस्कीत्यस्मिन्निम्नलिखितेभ्योऽनावृतस्रोतःप्रकल्पेभ्यो विध्यादेशाः सन्ति: + + + + + + प्रकल्पस्य जालसूत्रम् : +\n https://huskyapp.dev + + + + + अशुद्धीनामावेदनं वैशिष्ट्यनिवेदनञ्च +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + मास्टुडोनमाध्यमेन सम्प्रविश्यताम् + + + नवमास्टोडोनलेखा युज्यताम् + + + मास्टोडोने पञ्चनिमेषपरिमितो न्यूनतमः कालबद्धसमयः । + + + + + कस्याऽपि विशिष्टस्थलस्य सङ्केतसूत्रमत्र टङ्कयितुं शक्यते shitposter.club, blob.cat, expired.mentality.rip, तथेैवअधिकम् +\n +\nयदि युष्माकं व्यक्तिगतलेखाऽत्र न वर्तते तर्हि तस्य विशिष्टस्थलस्य नाम टङ्कयित्वा तत्र निर्मातुं शक्नुथ । +\n +\nविशिष्टस्थलमित्युक्ते स्थलमेकं यत्र युष्माकं लेखाः आश्रिताः, किन्तु साफल्येनैवाऽन्यविशिष्टस्थलीयैः सह सम्पर्कयितुं शक्यते । +\n +\nअधिकमत्र प्राप्यते joinmastodon.org. + + + diff --git a/app/src/husky/res/values-sk/husky_generated.xml b/app/src/husky/res/values-sk/husky_generated.xml new file mode 100644 index 0000000..dae1859 --- /dev/null +++ b/app/src/husky/res/values-sk/husky_generated.xml @@ -0,0 +1,11 @@ + + + + + + + Prihlásiť sa účtom Pleroma + + + + diff --git a/app/src/husky/res/values-sl/husky_generated.xml b/app/src/husky/res/values-sl/husky_generated.xml new file mode 100644 index 0000000..4a9ad83 --- /dev/null +++ b/app/src/husky/res/values-sl/husky_generated.xml @@ -0,0 +1,53 @@ + + + Husky %s + + + Husky je prosta in odprtokodna programska oprema. Licencirana je pod licenco GNU General Public License različice 3. Licenco si lahko ogledate tukaj: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Profil Husky + + + Če želite uveljaviti te spremembe, morate znova zagnati Husky + + + Husky vsebuje kodo in sredstva iz naslednjih odprtokodnih projektov: + + + Poganja ga Husky + + + + + + Spletna stran projekta: +\nhttps://huskyapp.dev + + + + + Poročila o napakah in želje za nove funkcije: +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + Prijavite se z Pleromaom + + + Dodaj nov Pleroma račun + + + + + Tu lahko vnesete naslov ali domeno katerega koli vozlišča, na primer shitposter.club, blob.cat, expired.mentality.rip in več! +\n +\nČe še nimate računa, lahko vnesete ime vozlišča, kateremu bi se radi pridružili, in tam ustvarite račun. +\n +\nVozlišče je ena lokacija, kjer je gostovanje vašega računa, vendar lahko preprosto komunicirate in sledite ljudem na drugih vozliščih, kot da bi bili na isti lokaciji. +\n +\nVeč informacij najdete na naslovu joinmastodon.org. + + + diff --git a/app/src/husky/res/values-sv/husky_generated.xml b/app/src/husky/res/values-sv/husky_generated.xml new file mode 100644 index 0000000..ecd46f0 --- /dev/null +++ b/app/src/husky/res/values-sv/husky_generated.xml @@ -0,0 +1,59 @@ + + + Husky %s + + + Husky är fri programvara med öppen källkod. Det är licensierat under GNU General Public License version 3. Du kan läsa mer om licensen här: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Huskys Profil + + + Du måste starta om Husky för att tillämpa ändringarna + + + Husky innehåller kod och tillgångar från följande öppen källkodsprojekt: + + + Drivs av Husky + + + + + + Tuskys webbsida:\n + https://huskyapp.dev + + + + + + Buggrapporter & funktionsförslag:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Logga in med Pleroma + + + Lägg till ett nytt Pleroma-konto + + + Pleroma har ett minimalt schemaläggningsintervall på 5 minuter. + + + + + Adressen eller domänen för varje instans kan anges + här, till exempel shitposter.club, blob.cat, expired.mentality.rip och + mer! + \n\nOm du inte har något konto kan du ange namnet på instansen du vill ansluta till och skapa ett konto där. + \n\nEn instans är en plats där ditt konto finns, men du kan enkelt kommunicera med och följa andra personer på andra instanser, + som om du var på samma sajt. + \n\nMer information finns på joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-sv/strings.xml b/app/src/husky/res/values-sv/strings.xml new file mode 100644 index 0000000..34387d1 --- /dev/null +++ b/app/src/husky/res/values-sv/strings.xml @@ -0,0 +1,69 @@ + + + Reagera + Ta bort reaktion + Vem reagerade + Aktivera %s + Avaktivera %s + %s reagerade av + Applikationsmanamn + Administratör + Klistermärken + Filstorleken är större än vad instanser tillåter + Ett fel inträffade vid hämtning av klistermärke + Emoji Reaktioner + Dölj tystade användare + Aktivera större anpassade emojis + Inläggssynlighet + Schemalägg inlägg + Repetera + Ta bort repetering + Dölj repeteringar + Visa repeteringar + SKICKA + SKICKA! + Svara till + Applikationswebbplats + Moderatorn + %s reagerade med %s på ditt inlägg + Aviseringar på nya emoji-reaktioner + Syntax på formatteringsstandard (om instansen stödjer det) + reaktioner på mina inlägg med emojis + Aktivera experimentell Pleroma-FE klistermärke (om möjligt) + Visa repeteringar + Schemalagda inlägg + Repetera till den ursprungliga målgruppen + Ta bort repetering + Öppna inlägg + Skriv inlägg + Ta bort detta inlägg\? + Öppna avsändaren av repeteringen + Repeterat + Radera och skriva en nytt inlägg\? + Fel vid sändning av inlägg. + %s upprepade ditt inlägg + %s favoriserade ditt inlägg + Upprepningar + Aviseringar när dina inlägg blir favoriserade + mina inlägg är repeterade + Visa upprepningar + Expandera alltid inlägg med innehållsvarningar + + %s Repeterade + %s Repeterades + + Dela inläggs-URL till… + Dela inlägg till… + Skickar inlägg… + Skickar inläggen + En kopia av inlägget har sparats i dina utkast + Dela innehåll av inlägg + Dela länk till inlägg + %s repeterade + Schemalagda inlägg + Upprepad av + Posta + Aviseringar när dina inlägg blir upprepade + Visa en bekräftelsedialog innan du repeterar + Fel vid sändning av inlägg + \ No newline at end of file diff --git a/app/src/husky/res/values-ta/husky_generated.xml b/app/src/husky/res/values-ta/husky_generated.xml new file mode 100644 index 0000000..227d1f1 --- /dev/null +++ b/app/src/husky/res/values-ta/husky_generated.xml @@ -0,0 +1,50 @@ + + + Husky(டஸ்கி) %s + + + Husky ஒரு கட்டற்ற மற்றும் திறந்த மூல மென்பொருள். இதன் உரிமம் GNU General Public License(பொது உரிமம்) பதிப்பு 3 -ன் கீழ் உள்ளது. நீங்கள் உரிமம் பற்றி காண: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky-ன் கணக்கு + + + இந்த மாறுதல்கள் செயற்படுத்த செயலியை மறுதொடக்கம் செய்ய வேண்டும் + + + Husky கொண்டுள்ள நிரல் மற்றும் துணுக்குகள் பின்வரும் திறந்த மூல திட்டங்கள்: + + + + + + திட்டத்தின் வலைத்தளம்:\n + https://huskyapp.dev + + + + + + பிழை அறிக்கைகள் & அம்ச கோரிக்கைகள்:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Pleroma மூலம் உள்நுழைய + + + புதிய Pleroma கணக்கைச் சேர்க்க + + + + + ஏதேனும் instance-ன் முகவரியையோ அல்லது களத்தின் முகவரியையோ இங்கு உள்ளிடவும், உதாரணமாக shitposter.club, blob.cat, expired.mentality.rip, மற்றும் + மேலும்! + \n\nபயனர் கணக்கு இல்லையெனில் புதிய கணக்கிற்கான instance(களம்)-னை பதிவிடவும். நீங்கள் குறிப்பிடப்படும் களத்தில் உங்கள் கணக்கு பதிவாகும்.\n\nமேலும் இங்கு குறிப்பிடப்பட்ட ஏதேனும் ஒரு களத்தில் மட்டுமே உங்களால் கணக்கு ஆரம்பித்துக்கொள்ள இயலும் இருப்பினும் நம்மால் மற்ற களங்களில் உள்ள நண்பர்களையும் தொடர்பு கொள்ள இயலும் . + \n\nமேலும் தகவல்கள் அறிய joinmastodon.org. + + + + diff --git a/app/src/husky/res/values-te/husky_generated.xml b/app/src/husky/res/values-te/husky_generated.xml new file mode 100644 index 0000000..0b7834d --- /dev/null +++ b/app/src/husky/res/values-te/husky_generated.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/husky/res/values-th/husky_generated.xml b/app/src/husky/res/values-th/husky_generated.xml new file mode 100644 index 0000000..201f22b --- /dev/null +++ b/app/src/husky/res/values-th/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky มีโค้ดและสินทรัพย์จากโครงการโอเพนซอร์สต่อไปนี้: + + + จำเป็นต้องเริ่ม Husky ใหม่ เพื่อใช้การเปลี่ยนแปลงเหล่านี้ + + + บัญชีทางการของ Husky + + + Husky คือซอฟต์แวร์เสรีและโอเพนซอร์ส ภายใต้สัญญาอนุญาต GNU General Public License Version 3 ดูสัญญาที่ : https://www.gnu.org/licenses/gpl-3.0.ja.html + + + ขับเคลื่อนด้วย Husky + + + Husky %s + + + + + + เว็บไซต์โปรเจกต์: +\nhttps://huskyapp.dev + + + + + รายงานช่องโหว่ และ ขอฟีเจอร์ (ภาษาอังกฤษ): +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + เพิ่มบัญชี Pleroma ใหม่ + + + เข้าสู่ระบบด้วย Pleroma + + + Pleroma กำหนดเวลาขั้นต่ำ 5 นาที + + + + + ใส่ที่อยู่หรือโดเมนของ Instance ได้ที่นี่ เช่น shitposter.club blob.cat expired.mentality.rip และ อีกมากมาย! +\n +\nถ้ายังไม่มีบัญชี สามารถใส่ชื่อ Instance ที่ต้องการจะร่วมแล้วสร้างบัญชีที่นั่น +\n +\nInstance คือที่ที่หนึ่งไว้โฮสต์บัญชีคุณ แต่คุณยังสามารถสื่อสาร ติดตามบุคคลบน Instance อื่นได้เหมือนอยู่บนไซต์เดียวกัน +\n +\nพบข้อมูลเพิ่มเติมได้ที่ joinmastodon.org + + + diff --git a/app/src/husky/res/values-th/strings.xml b/app/src/husky/res/values-th/strings.xml new file mode 100644 index 0000000..30c3e8f --- /dev/null +++ b/app/src/husky/res/values-th/strings.xml @@ -0,0 +1,68 @@ + + + ค่าปริยายของไวยากรณ์การจัดรูปแบบ (ถ้ารองรับโดย Instance) + เปิดใช้งานเอโมจิที่กำหนดเองขนาดใหญ่ + โพสต์ + โพสต์! + เปิดโพสต์ + เขียนโพสต์ + ลบโพสต์นี้ \? + เกิดข้อผิดพลาดในการส่งโพสต์ + รีพีต + เปิดดูผู้รีพีต + แสดงรีพีต + แสดงรีพีต + ซ่อนรีพีต + ลบรีพีต + รีพีตแล้ว + ซ่อนผู้ใช้ที่ปิดเสียงไว้ + การมองเห็นโพสต์ + โพสต์แบบกำหนดเวลา + รีพีต + การแจ้งเตือนเมื่อโพสต์ของคุณถูกชื่นชอบ + %s ชื่นชอบโพสต์คุณ + โพสต์ฉันถูกรีพีตแล้ว + แสดงรีพีต + ขยายโพสต์ที่มีเครื่องหมายคำเตือนเนื้อหาเสมอ + + <b>%s</b> รีพีต + + กำลังส่งโพสต์… + เกิดข้อผิดพลาดในการส่งโพสต์ + แบ่งปันเนื้อหาของโพสต์ + %s ได้รีพีต + การโต้ตอบเอโมจิ + แบ่งปัน URL โพสต์ไป… + โพสต์ + ตอบกลับ + การแจ้งเตือนเกี่ยวกับการโต้ตอบเอโมจิใหม่ + ลบ แล้ว ร่างโพสต์นี้ใหม่ \? + แบ่งปันโพสต์ไป… + โพสต์ที่กำหนดเวลา + โพสต์ของฉันถูกโต้ตอบโดยเอโมจิ + รีพีตโดย + เปิดใช้งานสติกเกอร์ Pleroma-FE รุ่นทดลอง (ถ้ามี) + %s รีพีตโพสต์คุณ + โต้ตอบแบบเอโมจิ + %s ถูกโต้ตอบโดย + กำลังส่งโพสต์ + ขนาดไฟล์เกินขีดจำกัดกว่าที่ Instance กำหนดไว้ + ลบรีพีต + การแจ้งเตือนเมื่อโพสต์คุณถูกรีพีต + สำเนาโพสต์ได้บันทึกลงในฉบับร่างแล้ว + %s โต้ตอบด้วย %s แก่โพสต์คุณ + แสดงข้อความยืนยันก่อนที่จะรีพีต + แบ่งปันลิงก์ของโพสต์ + รีพีตโพสต์ต้นตอ + เปิดใช้งาน %s + ปิดใช้งาน %s + สติกเกอร์ + ชื่อแอป + เว็บไซต์ของแอป + ผู้ดูแล + ผู้ควบคุม + เกิดข้อผิดพลาดขณะดึงข้อมูลสติกเกอร์ + ลบโต้ตอบแบบเอโมจิ + ผู้โต้ตอบ + โพสต์แบบตั้งเวลา + \ No newline at end of file diff --git a/app/src/husky/res/values-tr/husky_generated.xml b/app/src/husky/res/values-tr/husky_generated.xml new file mode 100644 index 0000000..733f379 --- /dev/null +++ b/app/src/husky/res/values-tr/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky %s + + + Husky özgür ve açık kaynak bir yazılımdır. GNU Genel Kamu Lisansı sürüm 3 altında lisanslanmıştır. Lisansı buradan görüntüleyebilirsiniz: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky\'nin Profili + + + Bu değişiklikleri uygulamak için Husky\'yi yeniden başlatmanız gerekecek + + + Husky aşağıdakı açık kaynaklı projelerden kod ve materyal içeriyor: + + + Husky tarafından desteklenmektedir + + + + + + Projenin internet sitesi: +\n https://huskyapp.dev + + + + + & özellik istekleri hata raporları: +\n https://git.mentality.rip/FWGS/Husky/issues + + + + + Pleroma ile giriş yap + + + Yeni Pleroma hesabı ekle + + + Pleroma\'un minimum 5 dakikalık zamanlama aralığı vardır. + + + + + Burada her hangi bir Mastodon sunucusunun adresi (shitposter.club, blob.cat, expired.mentality.rip, ve daha fazla!) girilebiliri. +\n +\nEğer hesabınız henüz yok ise katılmak istediğiniz sunucunun adresini girerek hesap yaratabilirsin. +\n +\nHer bir sunucu hesaplar ağırlayan bir yer olur ancak diğer sunucularda bulunan insanlarla aynı sitede olmuşcasına iletişime geçip takip edebilirsiniz. +\n +\nDaha fazla bilgi için shitposter.club. + + + diff --git a/app/src/husky/res/values-tr/strings.xml b/app/src/husky/res/values-tr/strings.xml new file mode 100644 index 0000000..69b967b --- /dev/null +++ b/app/src/husky/res/values-tr/strings.xml @@ -0,0 +1,68 @@ + + + Yanıtla + Tepki + Tepki Kaldır + Kim tepki gösterdi + Etkinleştir + Etiket + tarafından tepki verildi + Uygulama Adı + Uygulama Sitesi + Yönetici + Moderatör + Dosya boyutu varsayılan sınırları aşıyor + %s tepki verdi %s paylaşımınıza + Emoji Tepkileri + Yeni emoji tepkileri hakkında bildirimler + gönderilerim emoji ile tepki verildi + Susturulmuş kullanıcıları gizle + Daha büyük özel emojileri etkinleştir + Deneysel Pleroma-FE etiketlerini etkinleştir (varsa) + Yayın görünürlüğü + Yayını zamanla + Tekrarla + Tekrarlamayı Kaldır + Tekrarlamaları Gizle + YAYIN + YAYIN! + Tekrar yazarı aç + Tekrarlamaları Göster + Orijinal kitleye tekrarlayın + Tekrarlamaları Kaldır + Yayın aç + Tekrarlanmış + Yayını Silecekmisiniz\? + Bu gönderi silinsin mi ve taslağı yeniden çizilsin mi\? + % s yayınınızı tekrarladı + Tekrarlamalar + Yayınlarınız favori olarak işaretlendiğinde bildirim gelsin + Tekrarlamadan önce onay iletişim kutusunu göster + gönderilerim tekrarlandı + Gönderileri göster + Yayın Bağlantısını şurada paylaş… + Yayınını şurada paylaş… + Yayın gönderiliyor… + Yayın gönderilirken bir hata oluştu + Gönderi gönderme + Yayının içeriğini paylaşma + Yayının bağlantısını paylaş + %s Tekrarlandı + Zamanlanmış yayınlar + Şu kişi tarafından tekrarlandı + Yayın + Devre Dışı Bırak + Çıkartma getirilirken bir hata oluştu + Varsayılan biçimlendirme sözdizimi (örnek tarafından destekleniyorsa) + Tekrarlamaları Göster + Yazı Oluştur + Gönderi gönderilirken hata oluştu. + % s yayınınızı favorilere ekledi + Gönderileriniz tekrarlandığında bildirimler + Her zaman içerik uyarılarıyla işaretlenmiş yayınları genişletin + Yayının bir kopyası taslaklarınıza kaydedildi + + </b><b>%s</b> Tekrar + <b>%s</b> Tekrarlar + + \ No newline at end of file diff --git a/app/src/husky/res/values-uk/husky_generated.xml b/app/src/husky/res/values-uk/husky_generated.xml new file mode 100644 index 0000000..5ed7679 --- /dev/null +++ b/app/src/husky/res/values-uk/husky_generated.xml @@ -0,0 +1,11 @@ + + + + + + + Увійти + + + + diff --git a/app/src/husky/res/values-uk/strings.xml b/app/src/husky/res/values-uk/strings.xml new file mode 100644 index 0000000..4b377d0 --- /dev/null +++ b/app/src/husky/res/values-uk/strings.xml @@ -0,0 +1,21 @@ + + + Відповісти + Відреагувати + Прибрати реакцію + Хто відреагував + Назва програми + Веб сторінка програми + Адміністратор + Модератор + %s відреагував %s до вашого посту + Емодзі Реакції + Сповіщення про нові емодзі реакції + Приховати приглушених користувачів + Ввімкнути %s + Вимкнути %s + %s відреагував + Розмір файлу перевищує обмеження інстанції + Синтакс форматування за замовчуванням (якщо підтримується інстанцією) + мої пости мають емодзі реакції + diff --git a/app/src/husky/res/values-vi/husky_generated.xml b/app/src/husky/res/values-vi/husky_generated.xml new file mode 100644 index 0000000..28c1720 --- /dev/null +++ b/app/src/husky/res/values-vi/husky_generated.xml @@ -0,0 +1,56 @@ + + + Husky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Bạn có thể tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Powered by Husky + + + Husky %s + + + Trang cá nhân Husky + + + Husky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: + + + Bạn cần khởi động lại Husky để áp dụng các thiết lập + + + + + + Trang chủ +\nhttps://huskyapp.dev + + + + + Báo lỗi và đề xuất tính năng +\nhttps://git.mentality.rip/FWGS/Husky/issues + + + + + Đăng nhập Pleroma + + + Pleroma giới hạn tối thiểu 5 phút. + + + Thêm tài khoản Pleroma + + + + + Bạn phải nhập một tên miền, ví dụ shitposter.club, blob.cat, expired.mentality.rip, và nhiều hơn nữa! +\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. +\n +\nTham khảo thêm tại joinmastodon.org. + + + diff --git a/app/src/husky/res/values-zh-rCN/husky_generated.xml b/app/src/husky/res/values-zh-rCN/husky_generated.xml new file mode 100644 index 0000000..38523fe --- /dev/null +++ b/app/src/husky/res/values-zh-rCN/husky_generated.xml @@ -0,0 +1,60 @@ + + + Husky %s + + + Husky 是基于 GNU General Public License Version 3 许可证开源的自由软件。完整的许可证协议:https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 官方帐号 + + + 你需要重启 Husky 才能生效 + + + Husky 使用了以下开源项目的源码: + + + 由Husky提供支持 + + + + + + + 项目地址:\n + https://huskyapp.dev + + + + + + + 问题反馈:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + 登录 Pleroma 帐号 + + + 添加新的 Pleroma 帐号 + + + Pleroma的最小预订时间为5分钟。 + + + + + 请输入你帐号所在的 Mastodon 站点的域名,比如 shitposter.club,blob.cat,expired.mentality.rip,等等 。 +\n +\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Tusky 登入。 +\n +\n在 Mastodon 里,你的账号信息储存在某一特定实例当中,但 Mastodon 可使跨站互动和站内互动一样简单。 +\n +\n可以前往 https://joinmastodon.org 了解更多信息。 + + + diff --git a/app/src/husky/res/values-zh-rHK/husky_generated.xml b/app/src/husky/res/values-zh-rHK/husky_generated.xml new file mode 100644 index 0000000..fafdfb1 --- /dev/null +++ b/app/src/husky/res/values-zh-rHK/husky_generated.xml @@ -0,0 +1,45 @@ + + + Husky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 官方帳號 + + + 你需要重啟 Husky 才能生效 + + + Husky 使用了以下開源專案的原始碼: + + + + + + + 專案網站:\n + https://huskyapp.dev + + + + + + + 問題回報:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + 登入 Pleroma 帳號 + + + 加入新的 Pleroma 帳號 + + + + + 請輸入你帳號所在的 Mastodon 站點的域名或地址 + + + diff --git a/app/src/husky/res/values-zh-rMO/husky_generated.xml b/app/src/husky/res/values-zh-rMO/husky_generated.xml new file mode 100644 index 0000000..fafdfb1 --- /dev/null +++ b/app/src/husky/res/values-zh-rMO/husky_generated.xml @@ -0,0 +1,45 @@ + + + Husky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 官方帳號 + + + 你需要重啟 Husky 才能生效 + + + Husky 使用了以下開源專案的原始碼: + + + + + + + 專案網站:\n + https://huskyapp.dev + + + + + + + 問題回報:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + 登入 Pleroma 帳號 + + + 加入新的 Pleroma 帳號 + + + + + 請輸入你帳號所在的 Mastodon 站點的域名或地址 + + + diff --git a/app/src/husky/res/values-zh-rSG/husky_generated.xml b/app/src/husky/res/values-zh-rSG/husky_generated.xml new file mode 100644 index 0000000..94853a6 --- /dev/null +++ b/app/src/husky/res/values-zh-rSG/husky_generated.xml @@ -0,0 +1,51 @@ + + + Husky %s + + + Husky 是基于 GNU General Public License Version 3 许可证开源的自由软件。完整的许可证协议:https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 官方帐号 + + + 你需要重启 Husky 才能生效 + + + Husky 使用了以下开源项目的源码: + + + + + + + 项目地址:\n + https://huskyapp.dev + + + + + + + 问题反馈:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + 登录 Pleroma 帐号 + + + 添加新的 Pleroma 帐号 + + + + + 请输入你帐号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,等等 。 + \n\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Tusky 登入。 + \n\n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 https://joinmastodon.org 了解更多信息。 + + + + diff --git a/app/src/husky/res/values-zh-rTW/husky_generated.xml b/app/src/husky/res/values-zh-rTW/husky_generated.xml new file mode 100644 index 0000000..3840283 --- /dev/null +++ b/app/src/husky/res/values-zh-rTW/husky_generated.xml @@ -0,0 +1,48 @@ + + + Husky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky 官方帳號 + + + 你需要重啟 Husky 才能生效 + + + Husky 使用了以下開源專案的原始碼: + + + Husky %s + + + + + + + 專案網站:\n + https://huskyapp.dev + + + + + + + 問題回報:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + 登入 Pleroma 帳號 + + + 加入新的 Pleroma 帳號 + + + + + 請輸入你帳號所在的 Mastodon 站點的域名或地址 + + + diff --git a/app/src/husky/res/values/donottranslate.xml b/app/src/husky/res/values/donottranslate.xml new file mode 100644 index 0000000..3e342ab --- /dev/null +++ b/app/src/husky/res/values/donottranslate.xml @@ -0,0 +1,9 @@ + + + + + https://huskyapp.dev + + + + diff --git a/app/src/husky/res/values/husky_donottranslate.xml b/app/src/husky/res/values/husky_donottranslate.xml new file mode 100644 index 0000000..374fcc8 --- /dev/null +++ b/app/src/husky/res/values/husky_donottranslate.xml @@ -0,0 +1,19 @@ + + Plaintext + Markdown + BBCode + HTML + + + @string/action_plaintext + @string/action_markdown + @string/action_bbcode + @string/action_html + + + + + %1$s; %2$s; %3$s + + + diff --git a/app/src/husky/res/values/husky_generated.xml b/app/src/husky/res/values/husky_generated.xml new file mode 100644 index 0000000..960fca3 --- /dev/null +++ b/app/src/husky/res/values/husky_generated.xml @@ -0,0 +1,64 @@ + + + Husky %s + + + Powered by Husky + + + Husky is free and open-source software. + It is licensed under the GNU General Public License Version 3. + You can view the license here: https://www.gnu.org/licenses/gpl-3.0.en.html + + + Husky\'s Profile + + + You\'ll need to restart Husky in order to apply these changes + + + Husky contains code and assets from the following open source projects: + + + + + + + Project website:\n + https://huskyapp.dev + + + + + + + Bug reports & feature requests:\n + https://git.mentality.rip/FWGS/Husky/issues + + + + + + Login with Pleroma + + + Add new Pleroma Account + + + Pleroma has a minimum scheduling interval of 5 minutes. + + + + + The address or domain of any instance can be entered + here, such as shitposter.club, blob.cat, expired.mentality.rip, and + more! + \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to + join and create an account there.\n\nAn instance is a single place where your account is + hosted, but you can easily communicate with and follow folks on other instances as though + you were on the same site. + \n\nMore info can be found at joinmastodon.org. + + + + diff --git a/app/src/husky/res/values/strings.xml b/app/src/husky/res/values/strings.xml new file mode 100644 index 0000000..0c32bff --- /dev/null +++ b/app/src/husky/res/values/strings.xml @@ -0,0 +1,166 @@ + + + Chats + You + + Recipient does not support Chats + + Mark as read + Reply to + React + Remove reaction + Who reacted + Enable %s + Disable %s + Stickers + Open in external app + Open chat + Expand menu + + %s reacted by + + Application name + Application website + + Admin + Moderator + + File size exceeds instance limits + An error occurred while fetching sticker + + %s reacted with %s to your post + Emoji Reactions + Notifications about new emoji reactions + %s sent you a message + Chat Messages + Notifications about new chat messages + %s just posted + Subscriptions + Notifications when somebody you\'re subscribed to published a new post + %s migrated to + Move + Notifications when somebody you\'re following migrated to another profile + + Other + Privacy + + Anonymize uploaded file names + Live notifications + May slightly increase power consumption + Default formatting syntax(if supported by instance) + my posts are reacted with emojis + received a chat message + somebody I\'m subscribed to published a new post + somebody I\'m following migrated to another profile + Hide muted users + Enable bigger custom emojis + Enable experimental Pleroma-FE stickers(if available) + Animate custom emojis + Render subscriptions as normal posts + + Image + Video + Audio + Attachment + + Link + + Live notifications + Running live notifications for: + + + Post visibility + Schedule post + Repeat + Remove repeat + Hide repeats + Show repeats + POST + POST! + Open repeat author + Show repeats + Scheduled posts + Repeat to original audience + Remove repeat + Open post + + Compose Post + + + Repeated + + Delete this post? + Delete and re-draft this post? + + Error sending post. + + %s repeated your post + %s favorited your post + Repeats + Notifications when your posts get repeated + Notifications when your posts get marked as favorite + + Show confirmation dialog before repeating + my posts are repeated + Show repeats + Always expand posts marked with content warnings + + + <b>%s</b> Repeat + <b>%s</b> Repeats + + Share post URL to… + Share post to… + Sending post… + Error sending post + Sending posts + A copy of the post has been saved to your drafts + + Share content of post + Share link to post + %s repeated + Reply to %s + + Scheduled posts + Repeated by + Post + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7a7236d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_bbcode.svg b/app/src/main/ic_bbcode.svg new file mode 100644 index 0000000..a24790d --- /dev/null +++ b/app/src/main/ic_bbcode.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/src/main/ic_html.svg b/app/src/main/ic_html.svg new file mode 100644 index 0000000..3022409 --- /dev/null +++ b/app/src/main/ic_html.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..fa60d186b3a7440bb2e087ec7703a6b0bd7feea1 GIT binary patch literal 44818 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 QAprd7X&Y&kYdA#y4^7A=6aWAK literal 0 HcmV?d00001 diff --git a/app/src/main/ic_launcher.svg b/app/src/main/ic_launcher.svg new file mode 100644 index 0000000..6bc832c --- /dev/null +++ b/app/src/main/ic_launcher.svg @@ -0,0 +1,146 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher_.svg b/app/src/main/ic_launcher_.svg new file mode 100644 index 0000000..6f5d87d --- /dev/null +++ b/app/src/main/ic_launcher_.svg @@ -0,0 +1,121 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher_foreground.svg b/app/src/main/ic_launcher_foreground.svg new file mode 100644 index 0000000..20995ba --- /dev/null +++ b/app/src/main/ic_launcher_foreground.svg @@ -0,0 +1,107 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_sticker.svg b/app/src/main/ic_sticker.svg new file mode 100644 index 0000000..1132521 --- /dev/null +++ b/app/src/main/ic_sticker.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt new file mode 100644 index 0000000..f72fa45 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -0,0 +1,87 @@ +package com.keylesspalace.tusky + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.StringRes +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.method.LinkMovementMethod +import android.text.style.URLSpan +import android.text.util.Linkify +import android.view.MenuItem +import android.widget.TextView +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) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + setTitle(R.string.about_title_activity) + + versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) + + if(BuildConfig.CUSTOM_INSTANCE.isBlank()) { + 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) + + tuskyProfileButton.setOnClickListener { + viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) + } + + aboutLicensesButton.setOnClickListener { + startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) + } + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + +} + +private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { + + val text = SpannableString(context.getText(textId)) + + Linkify.addLinks(text, Linkify.WEB_URLS) + + val urlSpans = text.getSpans(0, text.length, URLSpan::class.java) + for (span in urlSpans) { + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + val flags = text.getSpanFlags(span) + + val customSpan = object : CustomURLSpan(span.url) {} + + text.removeSpan(span) + text.setSpan(customSpan, start, end, flags) + } + + setText(text) + 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 new file mode 100644 index 0000000..9deb2e1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -0,0 +1,974 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ArgbEvaluator +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Bundle +import android.text.Editable +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.viewModels +import androidx.annotation.ColorInt +import androidx.annotation.Px +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.emoji.text.EmojiCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.MarginPageTransformer +import com.bumptech.glide.Glide +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.CollapsingToolbarLayout +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import com.keylesspalace.tusky.adapter.AccountFieldAdapter +import com.keylesspalace.tusky.components.chat.ChatActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Field +import com.keylesspalace.tusky.entity.IdentityProof +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.pager.AccountPagerAdapter +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.showMuteAccountDialog +import com.keylesspalace.tusky.viewmodel.AccountViewModel +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +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_account.* +import kotlinx.android.synthetic.main.view_account_moved.* +import java.text.NumberFormat +import javax.inject.Inject +import kotlin.math.abs + +class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, LinkListener { + + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: AccountViewModel by viewModels { viewModelFactory } + + private val accountFieldAdapter = AccountFieldAdapter(this) + + private var followState: FollowState = FollowState.NOT_FOLLOWING + private var blocking: Boolean = false + private var muting: Boolean = false + private var blockingDomain: Boolean = false + private var showingReblogs: Boolean = false + private var subscribing: Boolean = false + private var loadedAccount: Account? = null + + private var animateAvatar: Boolean = false + + // fields for scroll animation + private var hideFab: Boolean = false + private var oldOffset: Int = 0 + @ColorInt + private var toolbarColor: Int = 0 + @ColorInt + private var statusBarColorTransparent: Int = 0 + @ColorInt + private var statusBarColorOpaque: Int = 0 + + private var avatarSize: Float = 0f + @Px + private var titleVisibleHeight: Int = 0 + private lateinit var domain: String + + private enum class FollowState { + NOT_FOLLOWING, + FOLLOWING, + REQUESTED + } + + private lateinit var adapter: AccountPagerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + loadResources() + makeNotificationBarTransparent() + setContentView(R.layout.activity_account) + + // Obtain information to fill out the profile. + viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) + + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) + animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) + hideFab = sharedPrefs.getBoolean("fabHide", false) + + setupToolbar() + setupTabs() + setupAccountViews() + setupRefreshLayout() + subscribeObservables() + + if (viewModel.isSelf) { + updateButtons() + saveNoteInfo.hide() + } else { + saveNoteInfo.visibility = View.INVISIBLE + } + } + + /** + * Load colors and dimensions from resources + */ + private fun loadResources() { + toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface) + statusBarColorTransparent = ContextCompat.getColor(this, R.color.transparent_statusbar_background) + statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark) + avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size) + titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height) + } + + /** + * Setup account widgets visibility and actions + */ + private fun setupAccountViews() { + // Initialise the default UI states. + accountAdminTextView.hide() + accountModeratorTextView.hide() + accountFloatingActionButton.hide() + accountFollowButton.hide() + accountMuteButton.hide() + accountFollowsYouTextView.hide() + + // setup the RecyclerView for the account fields + accountFieldList.isNestedScrollingEnabled = false + accountFieldList.layoutManager = LinearLayoutManager(this) + accountFieldList.adapter = accountFieldAdapter + + + val accountListClickListener = { v: View -> + val type = when (v.id) { + R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS + R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS + else -> throw AssertionError() + } + val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) + startActivityWithSlideInAnimation(accountListIntent) + } + accountFollowers.setOnClickListener(accountListClickListener) + accountFollowing.setOnClickListener(accountListClickListener) + + accountStatuses.setOnClickListener { + // Make nice ripple effect on tab + accountTabLayout.getTabAt(0)!!.select() + val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) + poorTabView.isPressed = true + accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) + } + + // If wellbeing mode is enabled, follow stats and posts count should be hidden + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + if (wellbeingEnabled) { + accountStatuses.hide() + accountFollowers.hide() + accountFollowing.hide() + } + + } + + /** + * Init timeline tabs + */ + private fun setupTabs() { + // Setup the tabs and timeline pager. + adapter = AccountPagerAdapter(this, viewModel.accountId) + + accountFragmentViewPager.adapter = adapter + 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 -> + tab.text = pageTitles[position] + }.attach() + + val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) + accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + + accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?) { + tab?.position?.let { position -> + (adapter.getFragment(position) as? ReselectableFragment)?.onReselect() + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + + override fun onTabSelected(tab: TabLayout.Tab?) {} + + }) + } + + private fun setupToolbar() { + // set toolbar top margin according to system window insets + accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> + val top = insets.systemWindowInsetTop + + val toolbarParams = accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + toolbarParams.topMargin = top + + insets.consumeSystemWindowInsets() + } + + // Setup the toolbar. + setSupportActionBar(accountToolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setDisplayShowTitleEnabled(false) + } + + val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation) + + val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) + toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT) + accountToolbar.background = toolbarBackground + + accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) + + val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply { + fillColor = ColorStateList.valueOf(toolbarColor) + elevation = appBarElevation + shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) + .build() + } + accountAvatarImageView.background = avatarBackground + + // Add a listener to change the toolbar icon color when it enters/exits its collapsed state. + accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { + + override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { + + if (verticalOffset == oldOffset) { + return + } + oldOffset = verticalOffset + + if (titleVisibleHeight + verticalOffset < 0) { + supportActionBar?.setDisplayShowTitleEnabled(true) + } else { + supportActionBar?.setDisplayShowTitleEnabled(false) + } + + if (hideFab && !viewModel.isSelf && !blocking) { + if (verticalOffset > oldOffset) { + accountFloatingActionButton.show() + } + if (verticalOffset < oldOffset) { + hideFabMenu() + accountFloatingActionButton.hide() + } + } + + val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize + + accountAvatarImageView.scaleX = scaledAvatarSize + accountAvatarImageView.scaleY = scaledAvatarSize + + accountAvatarImageView.visible(scaledAvatarSize > 0) + + val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f) + + window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int + + val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int + + toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor) + + swipeToRefreshLayout.isEnabled = verticalOffset == 0 + } + }) + + } + + private fun makeNotificationBarTransparent() { + val decorView = window.decorView + decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + window.statusBarColor = statusBarColorTransparent + } + + /** + * Subscribe to data loaded at the view model + */ + private fun subscribeObservables() { + viewModel.accountData.observe(this) { + when (it) { + is Success -> onAccountChanged(it.data) + is Error -> { + Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() + } + } + } + viewModel.relationshipData.observe(this) { + val relation = it?.data + if (relation != null) { + onRelationshipChanged(relation) + } + + if (it is Error) { + Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() + } + + } + viewModel.accountFieldData.observe(this) { + accountFieldAdapter.fields = it + accountFieldAdapter.notifyDataSetChanged() + } + viewModel.noteSaved.observe(this) { + saveNoteInfo.visible(it, View.INVISIBLE) + } + } + + /** + * Setup swipe to refresh layout + */ + private fun setupRefreshLayout() { + swipeToRefreshLayout.setOnRefreshListener { + viewModel.refresh() + adapter.refreshContent() + } + viewModel.isRefreshing.observe(this) { isRefreshing -> + swipeToRefreshLayout.isRefreshing = isRefreshing == true + } + 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) + + val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView) + LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) + + // accountFieldAdapter.fields = account.fields ?: emptyList() + accountFieldAdapter.emojis = account.emojis ?: emptyList() + accountFieldAdapter.notifyDataSetChanged() + + accountLockedImageView.visible(account.locked) + accountBadgeTextView.visible(account.bot) + // API can return user is both admin and mod + // but admin rights already implies moderator, so just ignore it + val isAdmin = account.pleroma?.isAdmin ?: false + accountAdminTextView.visible(isAdmin) + accountModeratorTextView.visible(!isAdmin && account.pleroma?.isModerator ?: false) + + updateAccountAvatar() + updateToolbar() + updateMovedAccount() + updateRemoteAccount() + updateAccountStats() + invalidateOptionsMenu() + + accountMuteButton.setOnClickListener { + viewModel.unmuteAccount() + updateMuteButton() + } + } + + /** + * Load account's avatar and header image + */ + private fun updateAccountAvatar() { + loadedAccount?.let { account -> + + loadAvatar( + account.avatar, + accountAvatarImageView, + resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), + animateAvatar + ) + + if(animateAvatar) { + Glide.with(this) + .load(account.header) + .centerCrop() + .into(accountHeaderImageView) + } else { + Glide.with(this) + .asBitmap() + .load(account.header) + .centerCrop() + .into(accountHeaderImageView) + } + + + accountAvatarImageView.setOnClickListener { avatarView -> + val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) + + avatarView.transitionName = account.avatar + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar) + + startActivity(intent, options.toBundle()) + } + } + } + + /** + * Update toolbar views for loaded account + */ + private fun updateToolbar() { + loadedAccount?.let { account -> + + val emojifiedName = account.name.emojify(account.emojis, accountToolbar, true) + + try { + supportActionBar?.title = EmojiCompat.get().process(emojifiedName) + } catch (e: IllegalStateException) { + supportActionBar?.title = emojifiedName + } + supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username) + } + } + + /** + * Update moved account info + */ + private fun updateMovedAccount() { + loadedAccount?.moved?.let { movedAccount -> + + accountMovedView?.show() + + // necessary because accountMovedView is now replaced in layout hierachy + findViewById(R.id.accountMovedViewLayout).setOnClickListener { + onViewAccount(movedAccount.id) + } + + accountMovedDisplayName.text = movedAccount.name + 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) + + 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) + } + + } + + /** + * Check is account remote and update info if so + */ + private fun updateRemoteAccount() { + loadedAccount?.let { account -> + if (account.isRemote()) { + accountRemoveView.show() + accountRemoveView.setOnClickListener { + LinkHelper.openLink(account.url, this) + } + } + } + } + + private fun FloatingActionButton.menuAnimate(show: Boolean) { + val height = this.height.toFloat() + + if(show) { + visibility = View.VISIBLE + alpha = 0.0f + translationY = height + + animate().setDuration(200) + .translationY(0.0f) + .alpha(1.0f) + .setListener(object : AnimatorListenerAdapter() {}) // seems listener is saved, so reset it here + .start() + } else { + animate().setDuration(200) + .translationY(height) + .alpha(0.0f) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + visibility = View.GONE + super.onAnimationEnd(animation) + } + }) + .start() + } + } + + private fun hideFabMenu() { + openedFabMenu = false + + accountFloatingActionButton.animate().setDuration(200) + .rotation(0.0f).start() + accountFloatingActionButtonChat.menuAnimate(openedFabMenu) + accountFloatingActionButtonMention.menuAnimate(openedFabMenu) + + } + + var openedFabMenu = false + private fun animateFabMenu() { + if(openedFabMenu) { + hideFabMenu() + } else { + openedFabMenu = true + + accountFloatingActionButton.animate().setDuration(200) + .rotation(135.0f).start() + accountFloatingActionButtonChat.menuAnimate(openedFabMenu) + accountFloatingActionButtonMention.menuAnimate(openedFabMenu) + } + } + + /** + * Update account stat info + */ + 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) + + accountFloatingActionButtonMention.setOnClickListener { mention() } + + if(account.pleroma?.acceptsChatMessages == true) { + accountFloatingActionButtonChat.setOnClickListener { + mastodonApi.createChat(account.id) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ + val intent = ChatActivity.getIntent(this@AccountActivity, it) + startActivityWithSlideInAnimation(intent) + }, { + Toast.makeText(this@AccountActivity, getString(R.string.error_generic), Toast.LENGTH_SHORT).show() + }) + } + } else { + accountFloatingActionButtonChat.backgroundTintList = ColorStateList.valueOf(Color.GRAY) + accountFloatingActionButtonChat.setOnClickListener { + Toast.makeText(this@AccountActivity, getString(R.string.error_chat_recipient_unavailable), Toast.LENGTH_SHORT).show() + } + } + + accountFloatingActionButton.setOnClickListener { animateFabMenu() } + + accountFollowButton.setOnClickListener { + if (viewModel.isSelf) { + val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) + startActivity(intent) + return@setOnClickListener + } + + if (blocking) { + viewModel.changeBlockState() + return@setOnClickListener + } + + when (followState) { + FollowState.NOT_FOLLOWING -> { + viewModel.changeFollowState() + } + FollowState.REQUESTED -> { + showFollowRequestPendingDialog() + } + FollowState.FOLLOWING -> { + showUnfollowWarningDialog() + } + } + updateFollowButton() + } + } + } + + private fun onRelationshipChanged(relation: Relationship) { + followState = when { + relation.following -> FollowState.FOLLOWING + relation.requested -> FollowState.REQUESTED + else -> FollowState.NOT_FOLLOWING + } + blocking = relation.blocking + muting = relation.muting + blockingDomain = relation.blockingDomain + showingReblogs = relation.showingReblogs + + // If wellbeing mode is enabled, "follows you" text should not be visible + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + 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 { + viewModel.changeSubscribingState() + } + if(relation.notifying != null) + subscribing = relation.notifying + else if(relation.subscribing != null) + subscribing = relation.subscribing + } + + accountNoteTextInputLayout.visible(relation.note != null) + accountNoteTextInputLayout.editText?.setText(relation.note) + + // add the listener late to avoid it firing on the first change + accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) + + updateButtons() + } + + private val noteWatcher = object: DefaultTextWatcher() { + override fun afterTextChanged(s: Editable) { + viewModel.noteChanged(s.toString()) + } + } + + private fun updateFollowButton() { + if (viewModel.isSelf) { + accountFollowButton.setText(R.string.action_edit_own_profile) + return + } + if (blocking) { + accountFollowButton.setText(R.string.action_unblock) + return + } + when (followState) { + FollowState.NOT_FOLLOWING -> { + accountFollowButton.setText(R.string.action_follow) + } + FollowState.REQUESTED -> { + accountFollowButton.setText(R.string.state_follow_requested) + } + FollowState.FOLLOWING -> { + accountFollowButton.setText(R.string.action_unfollow) + } + } + updateSubscribeButton() + } + + private fun updateMuteButton() { + if (muting) { + accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) + } else { + accountMuteButton.hide() + } + } + + private fun updateSubscribeButton() { + if(followState != FollowState.FOLLOWING) { + accountSubscribeButton.hide() + } + + if(subscribing) { + accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + } else { + accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + } + } + + private fun updateButtons() { + invalidateOptionsMenu() + + if (loadedAccount?.moved == null) { + + accountFollowButton.show() + updateFollowButton() + + if (blocking || viewModel.isSelf) { + hideFabMenu() + accountFloatingActionButton.hide() + accountMuteButton.hide() + accountSubscribeButton.hide() + } else { + accountFloatingActionButton.show() + if (muting) + accountMuteButton.show() + else + accountMuteButton.hide() + updateMuteButton() + } + + } else { + hideFabMenu() + accountFloatingActionButton.hide() + accountFollowButton.hide() + accountMuteButton.hide() + accountSubscribeButton.hide() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.account_toolbar, menu) + + if (!viewModel.isSelf) { + val follow = menu.findItem(R.id.action_follow) + follow.title = if (followState == FollowState.NOT_FOLLOWING) { + getString(R.string.action_follow) + } else { + getString(R.string.action_unfollow) + } + + follow.isVisible = followState != FollowState.REQUESTED + + val block = menu.findItem(R.id.action_block) + block.title = if (blocking) { + getString(R.string.action_unblock) + } else { + getString(R.string.action_block) + } + + val mute = menu.findItem(R.id.action_mute) + mute.title = if (muting) { + getString(R.string.action_unmute) + } else { + getString(R.string.action_mute) + } + + if (loadedAccount != null) { + val muteDomain = menu.findItem(R.id.action_mute_domain) + domain = LinkHelper.getDomain(loadedAccount?.url) + if (domain.isEmpty()) { + // If we can't get the domain, there's no way we can mute it anyway... + menu.removeItem(R.id.action_mute_domain) + } else { + if (blockingDomain) { + muteDomain.title = getString(R.string.action_unmute_domain, domain) + } else { + muteDomain.title = getString(R.string.action_mute_domain, domain) + } + } + } + + if (followState == FollowState.FOLLOWING) { + val showReblogs = menu.findItem(R.id.action_show_reblogs) + showReblogs.title = if (showingReblogs) { + getString(R.string.action_hide_reblogs) + } else { + getString(R.string.action_show_reblogs) + } + + } else { + menu.removeItem(R.id.action_show_reblogs) + } + + } else { + // It shouldn't be possible to block, follow, mute or report yourself. + menu.removeItem(R.id.action_follow) + menu.removeItem(R.id.action_block) + menu.removeItem(R.id.action_mute) + menu.removeItem(R.id.action_mute_domain) + menu.removeItem(R.id.action_show_reblogs) + menu.removeItem(R.id.action_report) + } + + return super.onCreateOptionsMenu(menu) + } + + private fun showFollowRequestPendingDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.dialog_message_cancel_follow_request) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showUnfollowWarningDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.dialog_unfollow_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun toggleBlockDomain(instance: String) { + if(blockingDomain) { + viewModel.unblockDomain(instance) + } else { + AlertDialog.Builder(this) + .setMessage(getString(R.string.mute_domain_warning, instance)) + .setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun toggleBlock() { + if (viewModel.relationshipData.value?.data?.blocking != true) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else { + viewModel.changeBlockState() + } + } + + private fun toggleMute() { + if (viewModel.relationshipData.value?.data?.muting != true) { + loadedAccount?.let { + showMuteAccountDialog( + this, + it.username + ) { notifications, duration -> + viewModel.muteAccount(notifications, duration) + } + } + } else { + viewModel.unmuteAccount() + } + } + + private fun mention() { + loadedAccount?.let { + val intent = ComposeActivity.startIntent(this, + ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))) + startActivity(intent) + } + } + + override fun onViewTag(tag: String) { + val intent = Intent(this, ViewTagActivity::class.java) + intent.putExtra("hashtag", tag) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewAccount(id: String) { + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra("id", id) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewUrl(url: String) { + viewUrl(url) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.action_mention -> { + mention() + return true + } + R.id.action_open_in_web -> { + // If the account isn't loaded yet, eat the input. + if (loadedAccount != null) { + LinkHelper.openLink(loadedAccount?.url, this) + } + return true + } + R.id.action_follow -> { + viewModel.changeFollowState() + return true + } + R.id.action_block -> { + toggleBlock() + return true + } + R.id.action_mute -> { + toggleMute() + return true + } + R.id.action_mute_domain -> { + toggleBlockDomain(domain) + return true + } + R.id.action_show_reblogs -> { + viewModel.changeShowReblogsState() + return true + } + R.id.action_report -> { + if (loadedAccount != null) { + startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount!!.username)) + } + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun getActionButton(): FloatingActionButton? { + return if (!viewModel.isSelf && !blocking) { + accountFloatingActionButton + } else null + } + + override fun onActionButtonHidden() { + hideFabMenu() + } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + + private const val KEY_ACCOUNT_ID = "id" + private val argbEvaluator = ArgbEvaluator() + + @JvmStatic + fun getIntent(context: Context, accountId: String): Intent { + val intent = Intent(context, AccountActivity::class.java) + intent.putExtra(KEY_ACCOUNT_ID, accountId) + return intent + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt new file mode 100644 index 0000000..33f9209 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -0,0 +1,105 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +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 { + + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + + enum class Type { + FOLLOWS, + FOLLOWERS, + BLOCKS, + MUTES, + FOLLOW_REQUESTS, + REBLOGGED, + FAVOURITED, + REACTED + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_account_list) + + val type = intent.getSerializableExtra(EXTRA_TYPE) as Type + val id: String? = intent.getStringExtra(EXTRA_ID) + val emoji: String? = intent.getStringExtra(EXTRA_EMOJI) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + when (type) { + Type.BLOCKS -> setTitle(R.string.title_blocks) + Type.MUTES -> setTitle(R.string.title_mutes) + Type.FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) + Type.FOLLOWERS -> setTitle(R.string.title_followers) + Type.FOLLOWS -> setTitle(R.string.title_follows) + Type.REBLOGGED -> setTitle(R.string.title_reblogged_by) + Type.FAVOURITED -> setTitle(R.string.title_favourited_by) + Type.REACTED -> setTitle(String.format(getString(R.string.title_emoji_reacted_by), emoji)) + } + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, emoji)) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + private const val EXTRA_TYPE = "type" + private const val EXTRA_ID = "id" + private const val EXTRA_EMOJI = "emoji" + + @JvmStatic + fun newIntent(context: Context, type: Type, id: String?, emoji: String?): Intent { + return Intent(context, AccountListActivity::class.java).apply { + putExtra(EXTRA_TYPE, type) + putExtra(EXTRA_ID, id) + putExtra(EXTRA_EMOJI, emoji) + } + } + + @JvmStatic + fun newIntent(context: Context, type: Type, id: String? = null): Intent { + return newIntent(context, type, id, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt new file mode 100644 index 0000000..71aeb1b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -0,0 +1,285 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.DialogFragment +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.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel +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 + +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 lateinit var listId: String + private lateinit var listName: String + private val adapter = Adapter() + private val searchAdapter = SearchAdapter() + + private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } + private val animateAvatar by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("animateGifAvatars", false) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) + viewModel = viewModelFactory.create(AccountsInListViewModel::class.java) + val args = arguments!! + listId = args.getString(LIST_ID_ARG)!! + listName = args.getString(LIST_NAME_ARG)!! + + viewModel.load(listId) + } + + override fun onStart() { + super.onStart() + dialog?.apply { + // Stretch dialog to the window + window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_accounts_in_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + accountsRecycler.layoutManager = LinearLayoutManager(view.context) + accountsRecycler.adapter = adapter + + accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) + accountsSearchRecycler.adapter = searchAdapter + + viewModel.state + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { state -> + adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) + + when (state.accounts) { + is Either.Right -> messageView.hide() + is Either.Left -> handleError(state.accounts.value) + } + + setupSearchView(state) + } + + searchView.isSubmitButtonEnabled = true + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + viewModel.search(query ?: "") + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + // Close event is not sent so we use this instead + if (newText.isNullOrBlank()) { + viewModel.search("") + } + return true + } + }) + } + + private fun setupSearchView(state: State) { + if (state.searchResult == null) { + searchAdapter.submitList(listOf()) + accountsSearchRecycler.hide() + 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() + } + } + + private fun handleError(error: Throwable) { + messageView.show() + val retryAction = { _: View -> + messageView.hide() + viewModel.load(listId) + } + if (error is IOException) { + messageView.setup(R.drawable.elephant_offline, + R.string.error_network, retryAction) + } else { + messageView.setup(R.drawable.elephant_error, + R.string.error_generic, retryAction) + } + } + + private fun onRemoveFromList(accountId: String) { + viewModel.deleteAccountFromList(listId, accountId) + } + + private fun onAddToList(account: Account) { + viewModel.addAccountToList(listId, account) + } + + private object AccountDiffer : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean { + return oldItem.deepEquals(newItem) + } + } + + 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 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) + usernameTextView.text = account.username + loadAvatar(account.avatar, avatar, radius, animateAvatar) + } + + override fun onClick(v: View?) { + onRemoveFromList(getItem(adapterPosition).id) + } + } + } + + private object SearchDiffer : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { + return oldItem.second == newItem.second + && oldItem.first.deepEquals(newItem.first) + } + + } + + 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 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) + usernameTextView.text = account.username + loadAvatar(account.avatar, avatar, radius, animateAvatar) + + rejectButton.apply { + if (inAList) { + setImageResource(R.drawable.ic_reject_24dp) + contentDescription = getString(R.string.action_remove_from_list) + } else { + setImageResource(R.drawable.ic_plus_24dp) + contentDescription = getString(R.string.action_add_to_list) + } + } + } + + init { + acceptButton.hide() + rejectButton.setOnClickListener(this) + } + + override fun onClick(v: View?) { + val (account, inAList) = getItem(adapterPosition) + if (inAList) { + onRemoveFromList(account.id) + } else { + onAddToList(account) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java new file mode 100644 index 0000000..3638726 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -0,0 +1,222 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; + +import com.google.android.material.snackbar.Snackbar; +import com.keylesspalace.tusky.adapter.AccountSelectionAdapter; +import com.keylesspalace.tusky.db.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.interfaces.AccountSelectionListener; +import com.keylesspalace.tusky.interfaces.PermissionRequester; +import com.keylesspalace.tusky.util.ThemeUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import javax.inject.Inject; + +public abstract class BaseActivity extends AppCompatActivity implements Injectable { + + @Inject + public AccountManager accountManager; + + private static final int REQUESTER_NONE = Integer.MAX_VALUE; + private HashMap requesters; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + + /* There isn't presently a way to globally change the theme of a whole application at + * runtime, just individual activities. So, each activity has to set its theme before any + * views are created. */ + String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT); + Log.d("activeTheme", theme); + if (theme.equals("black")) { + setTheme(R.style.TuskyBlackTheme); + } + + /* set the taskdescription programmatically, the theme would turn it blue */ + String appName = getString(R.string.app_name); + Bitmap appIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); + int recentsBackgroundColor = ThemeUtils.getColor(this, R.attr.colorSurface); + + setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor)); + + int style = textStyle(preferences.getString("statusTextSize", "medium")); + getTheme().applyStyle(style, false); + + if(requiresLogin()) { + redirectIfNotLoggedIn(); + } + + requesters = new HashMap<>(); + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base)); + } + + protected boolean requiresLogin() { + return true; + } + + private static int textStyle(String name) { + int style; + switch (name) { + case "smallest": + style = R.style.TextSizeSmallest; + break; + case "small": + style = R.style.TextSizeSmall; + break; + case "medium": + default: + style = R.style.TextSizeMedium; + break; + case "large": + style = R.style.TextSizeLarge; + break; + case "largest": + style = R.style.TextSizeLargest; + break; + } + return style; + } + + public void startActivityWithSlideInAnimation(Intent intent) { + super.startActivity(intent); + overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); + } + + @Override + public void finish() { + super.finish(); + overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right); + } + + public void finishWithoutSlideOutAnimation() { + super.finish(); + } + + protected void redirectIfNotLoggedIn() { + AccountEntity account = accountManager.getActiveAccount(); + if (account == null) { + Intent intent = new Intent(this, LoginActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityWithSlideInAnimation(intent); + finish(); + } + } + + protected void showErrorDialog(View anyView, @StringRes int descriptionId, @StringRes int actionId, View.OnClickListener listener) { + if (anyView != null) { + Snackbar bar = Snackbar.make(anyView, getString(descriptionId), Snackbar.LENGTH_SHORT); + bar.setAction(actionId, listener); + bar.show(); + } + } + + public void showAccountChooserDialog(CharSequence dialogTitle, boolean showActiveAccount, AccountSelectionListener listener) { + List accounts = accountManager.getAllAccountsOrderedByActive(); + AccountEntity activeAccount = accountManager.getActiveAccount(); + + switch(accounts.size()) { + case 1: + listener.onAccountSelected(activeAccount); + return; + case 2: + if (!showActiveAccount) { + for (AccountEntity account : accounts) { + if (activeAccount != account) { + listener.onAccountSelected(account); + return; + } + } + } + break; + } + + if (!showActiveAccount && activeAccount != null) { + accounts.remove(activeAccount); + } + AccountSelectionAdapter adapter = new AccountSelectionAdapter(this); + adapter.addAll(accounts); + + new AlertDialog.Builder(this) + .setTitle(dialogTitle) + .setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index))) + .show(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requesters.containsKey(requestCode)) { + PermissionRequester requester = requesters.remove(requestCode); + requester.onRequestPermissionsResult(permissions, grantResults); + } + } + + public void requestPermissions(String[] permissions, PermissionRequester requester) { + ArrayList permissionsToRequest = new ArrayList<>(); + for(String permission: permissions) { + if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + permissionsToRequest.add(permission); + } + } + if (permissionsToRequest.isEmpty()) { + int[] permissionsAlreadyGranted = new int[permissions.length]; + for (int i = 0; i < permissionsAlreadyGranted.length; ++i) + permissionsAlreadyGranted[i] = PackageManager.PERMISSION_GRANTED; + requester.onRequestPermissionsResult(permissions, permissionsAlreadyGranted); + return; + } + + int newKey = requester == null ? REQUESTER_NONE : requesters.size(); + if (newKey != REQUESTER_NONE) { + requesters.put(newKey, requester); + } + String[] permissionsCopy = new String[permissionsToRequest.size()]; + permissionsToRequest.toArray(permissionsCopy); + ActivityCompat.requestPermissions(this, permissionsCopy, newKey); + + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt new file mode 100644 index 0000000..6f3d279 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -0,0 +1,225 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.keylesspalace.tusky.components.chat.ChatActivity +import com.keylesspalace.tusky.entity.Chat +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.LinkHelper +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider +import com.uber.autodispose.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import java.net.URI +import java.net.URISyntaxException +import javax.inject.Inject + +/** this is the base class for all activities that open links + * links are checked against the api if they are mastodon links so they can be openend in Tusky + * Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierachy + */ + +abstract class BottomSheetActivity : BaseActivity() { + + lateinit var bottomSheet: BottomSheetBehavior + var searchUrl: String? = null + + @Inject + lateinit var mastodonApi: MastodonApi + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + val bottomSheetLayout: LinearLayout = findViewById(R.id.item_status_bottom_sheet) + bottomSheet = BottomSheetBehavior.from(bottomSheetLayout) + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + cancelActiveSearch() + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + + } + + open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) { + if (!looksLikeMastodonUrl(url)) { + openLink(url) + return + } + + mastodonApi.searchObservable( + query = url, + resolve = true + ).observeOn(AndroidSchedulers.mainThread()) + .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ (accounts, statuses) -> + if (getCancelSearchRequested(url)) { + return@subscribe + } + + onEndSearch(url) + + if (accounts.isNotEmpty()) { + + // HACKHACK: Pleroma, remove when search will work normally + if (accounts[0].pleroma != null) { + val account = accounts.firstOrNull { it.pleroma?.apId == url || it.url == url } + + if (account != null) { + viewAccount(account.id) + return@subscribe + } + } else { + viewAccount(accounts[0].id) + return@subscribe + } + } + + if (statuses.isNotEmpty()) { + viewThread(statuses[0].id, statuses[0].url) + return@subscribe + } + + performUrlFallbackAction(url, lookupFallbackBehavior) + }, { + if (!getCancelSearchRequested(url)) { + onEndSearch(url) + performUrlFallbackAction(url, lookupFallbackBehavior) + } + }) + + onBeginSearch(url) + } + + open fun viewThread(statusId: String, url: String?) { + if (!isSearching()) { + val intent = ViewThreadActivity.startIntent(this, statusId, url) + startActivityWithSlideInAnimation(intent) + } + } + + open fun viewAccount(id: String) { + val intent = AccountActivity.getIntent(this, id) + startActivityWithSlideInAnimation(intent) + } + + open fun openChat(chat: Chat) { + startActivityWithSlideInAnimation(ChatActivity.getIntent(this, chat)) + } + + protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) { + when (fallbackBehavior) { + PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url) + PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(this, getString(R.string.post_lookup_error_format, url), Toast.LENGTH_SHORT).show() + } + } + + @VisibleForTesting + fun onBeginSearch(url: String) { + searchUrl = url + showQuerySheet() + } + + @VisibleForTesting + fun getCancelSearchRequested(url: String): Boolean { + return url != searchUrl + } + + @VisibleForTesting + fun isSearching(): Boolean { + return searchUrl != null + } + + @VisibleForTesting + fun onEndSearch(url: String?) { + if (url == searchUrl) { + // Don't clear query if there's no match, + // since we might just now be getting the response for a canceled search + searchUrl = null + hideQuerySheet() + } + } + + @VisibleForTesting + fun cancelActiveSearch() { + if (isSearching()) { + onEndSearch(searchUrl) + } + } + + @VisibleForTesting + open fun openLink(url: String) { + LinkHelper.openLink(url, this) + } + + private fun showQuerySheet() { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun hideQuerySheet() { + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + } +} + +// https://mastodon.foo.bar/@User +// https://mastodon.foo.bar/@User/43456787654678 +// https://pleroma.foo.bar/users/User +// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0 +// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc +// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://friendica.foo.bar/profile/user +// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://misskey.foo.bar/notes/83w6r388br (always lowercase) +fun looksLikeMastodonUrl(urlString: String): Boolean { + val uri: URI + try { + uri = URI(urlString) + } catch (e: URISyntaxException) { + return false + } + + if (uri.query != null || + uri.fragment != null || + uri.path == null) { + return false + } + + val path = uri.path + return path.matches("^/@[^/]+$".toRegex()) || + path.matches("^/@[^/]+/\\d+$".toRegex()) || + path.matches("^/users/\\w+$".toRegex()) || + path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) || + path.matches("^/objects/[-a-f0-9]+$".toRegex()) || + path.matches("^/notes/[a-z0-9]+$".toRegex()) || + path.matches("^/display/[-a-f0-9]+$".toRegex()) || + path.matches("^/profile/\\w+$".toRegex()) +} + +enum class PostLookupFallbackBehavior { + OPEN_IN_BROWSER, + DISPLAY_ERROR, +} diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt new file mode 100644 index 0000000..f9b2c5a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -0,0 +1,427 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import androidx.activity.viewModels +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +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.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewmodel.EditProfileViewModel +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 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 { + + companion object { + const val AVATAR_SIZE = 400 + const val HEADER_WIDTH = 1500 + const val HEADER_HEIGHT = 500 + + private const val AVATAR_PICK_RESULT = 1 + private const val HEADER_PICK_RESULT = 2 + private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 + private const val MASTODON_MAX_ACCOUNT_FIELDS = 4 + + private const val BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING" + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: EditProfileViewModel by viewModels { viewModelFactory } + + private var currentlyPicking: PickType = PickType.NOTHING + + private val accountFieldEditAdapter = AccountFieldEditAdapter() + private var maxAccountFields = MASTODON_MAX_ACCOUNT_FIELDS + + private enum class PickType { + NOTHING, + AVATAR, + HEADER + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let { + currentlyPicking = PickType.valueOf(it) + } + + setContentView(R.layout.activity_edit_profile) + + setSupportActionBar(toolbar) + supportActionBar?.run { + setTitle(R.string.title_edit_profile) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } + headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } + + fieldList.layoutManager = LinearLayoutManager(this) + fieldList.adapter = accountFieldEditAdapter + + val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE } + + addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null) + + addFieldButton.setOnClickListener { + accountFieldEditAdapter.addField() + if(accountFieldEditAdapter.itemCount >= maxAccountFields) { + it.isVisible = false + } + + scrollView.post{ + scrollView.smoothScrollTo(0, it.bottom) + } + } + + viewModel.obtainProfile() + + viewModel.profileData.observe(this) { profileRes -> + when (profileRes) { + is Success -> { + val me = profileRes.data + if (me != null) { + + displayNameEditText.setText(me.displayName) + noteEditText.setText(me.source?.note) + lockedCheckBox.isChecked = me.locked + + accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) + addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < maxAccountFields + + if(viewModel.avatarData.value == null) { + Glide.with(this) + .load(me.avatar) + .placeholder(R.drawable.avatar_default) + .transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ) + .into(avatarPreview) + } + + if(viewModel.headerData.value == null) { + Glide.with(this) + .load(me.header) + .into(headerPreview) + } + + } + } + is Error -> { + val snackbar = Snackbar.make(avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) + snackbar.setAction(R.string.action_retry) { + viewModel.obtainProfile() + } + snackbar.show() + + } + } + } + + viewModel.obtainInstance() + viewModel.instanceData.observe(this) { result -> + when (result) { + is Success -> { + val instance = result.data + if (instance?.maxBioChars != null && instance.maxBioChars > 0) { + noteEditTextLayout.counterMaxLength = instance.maxBioChars + } + + instance?.pleroma?.metadata?.fieldsLimits?.let { + maxAccountFields = it.maxFields + + if(maxAccountFields > MASTODON_MAX_ACCOUNT_FIELDS + && accountFieldEditAdapter.itemCount == MASTODON_MAX_ACCOUNT_FIELDS + && !addFieldButton.isEnabled) { + addFieldButton.isEnabled = true + } + } + } + } + } + + observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true) + observeImage(viewModel.headerData, headerPreview, headerProgressBar, false) + + viewModel.saveData.observe(this) { + when(it) { + is Success -> { + finish() + } + is Loading -> { + saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) + } + } + } + + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString()) + } + + override fun onStop() { + super.onStop() + if(!isFinishing) { + viewModel.updateProfile(displayNameEditText.text.toString(), + noteEditText.text.toString(), + lockedCheckBox.isChecked, + accountFieldEditAdapter.getFieldData()) + } + } + + private fun observeImage(liveData: LiveData>, + imageView: ImageView, + progressBar: View, + roundedCorners: Boolean) { + liveData.observe(this, Observer> { + + when (it) { + is Success -> { + val glide = Glide.with(imageView) + .load(it.data) + + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ) + } + + glide.into(imageView) + + imageView.show() + progressBar.hide() + } + is Loading -> { + progressBar.show() + } + is Error -> { + progressBar.hide() + if(!it.consumed) { + onResizeFailure() + it.consumed = true + } + + } + } + }) + } + + private fun onMediaPick(pickType: PickType) { + if (currentlyPicking != PickType.NOTHING) { + // Ignore inputs if another pick operation is still occurring. + return + } + currentlyPicking = pickType + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + } else { + initiateMediaPicking() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + when (requestCode) { + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initiateMediaPicking() + } else { + endMediaPicking() + Snackbar.make(avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() + } + } + } + } + + private fun initiateMediaPicking() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + when (currentlyPicking) { + PickType.AVATAR -> { + startActivityForResult(intent, AVATAR_PICK_RESULT) + } + PickType.HEADER -> { + startActivityForResult(intent, HEADER_PICK_RESULT) + } + PickType.NOTHING -> { /* do nothing */ } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.edit_profile_toolbar, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.action_save -> { + save() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun save() { + if (currentlyPicking != PickType.NOTHING) { + return + } + + viewModel.save(displayNameEditText.text.toString(), + noteEditText.text.toString(), + 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 + } + + private fun beginMediaPicking() { + when (currentlyPicking) { + PickType.AVATAR -> { + avatarProgressBar.visibility = View.VISIBLE + avatarPreview.visibility = View.INVISIBLE + avatarButton.setImageDrawable(null) + + } + PickType.HEADER -> { + headerProgressBar.visibility = View.VISIBLE + headerPreview.visibility = View.INVISIBLE + headerButton.setImageDrawable(null) + } + PickType.NOTHING -> { /* do nothing */ } + } + } + + private fun endMediaPicking() { + avatarProgressBar.visibility = View.GONE + headerProgressBar.visibility = View.GONE + + currentlyPicking = PickType.NOTHING + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + AVATAR_PICK_RESULT -> { + if (resultCode == Activity.RESULT_OK && data != null) { + CropImage.activity(data.data) + .setInitialCropWindowPaddingRatio(0f) + .setOutputCompressFormat(Bitmap.CompressFormat.PNG) + .setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) + .start(this) + } else { + endMediaPicking() + } + } + HEADER_PICK_RESULT -> { + if (resultCode == Activity.RESULT_OK && data != null) { + CropImage.activity(data.data) + .setInitialCropWindowPaddingRatio(0f) + .setOutputCompressFormat(Bitmap.CompressFormat.PNG) + .setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) + .start(this) + } else { + endMediaPicking() + } + } + CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> { + val result = CropImage.getActivityResult(data) + when (resultCode) { + Activity.RESULT_OK -> beginResize(result.uri) + CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure() + else -> endMediaPicking() + } + } + } + } + + private fun beginResize(uri: Uri) { + beginMediaPicking() + + when (currentlyPicking) { + PickType.AVATAR -> { + viewModel.newAvatar(uri, this) + } + PickType.HEADER -> { + viewModel.newHeader(uri, this) + } + else -> { + throw AssertionError("PickType not set.") + } + } + + currentlyPicking = PickType.NOTHING + + } + + private fun onResizeFailure() { + Snackbar.make(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 new file mode 100644 index 0000000..55a856c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -0,0 +1,219 @@ +package com.keylesspalace.tusky + +import android.os.Bundle +import android.view.MenuItem +import android.widget.AdapterView +import android.widget.ArrayAdapter +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.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 okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject + +class FiltersActivity: BaseActivity() { + @Inject + lateinit var api: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + 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" + } + + private fun updateFilter(filter: Filter, itemIndex: Int) { + api.updateFilter(filter.id, MastodonApi.PostFilter(filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)) + .enqueue(object: Callback{ + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + val updatedFilter = response.body()!! + if (updatedFilter.context.contains(context)) { + filters[itemIndex] = updatedFilter + } else { + filters.removeAt(itemIndex) + } + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) + } + }) + } + + private fun deleteFilter(itemIndex: Int) { + val filter = filters[itemIndex] + if (filter.context.size == 1) { + // This is the only context for this filter; delete it + api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback { + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() + } + + override fun onResponse(call: Call, response: Response) { + filters.removeAt(itemIndex) + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) + } + }) + } else { + // Keep the filter, but remove it from this context + val oldFilter = filters[itemIndex] + val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context }, + oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord) + updateFilter(newFilter, itemIndex) + } + } + + private fun createFilter(phrase: String, wholeWord: Boolean) { + api.createFilter(MastodonApi.PostFilter(phrase, listOf(context), false, wholeWord, "")) + .enqueue(object: Callback { + override fun onResponse(call: Call, response: Response) { + val filterResponse = response.body() + if(response.isSuccessful && filterResponse != null) { + filters.add(filterResponse) + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) + } else { + Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show() + } + } + + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show() + } + }) + } + + private fun showAddFilterDialog() { + dialog = AlertDialog.Builder(this@FiltersActivity) + .setTitle(R.string.filter_addition_dialog_title) + .setView(R.layout.dialog_filter) + .setPositiveButton(android.R.string.ok){ _, _ -> + createFilter(dialog.phraseEditText.text.toString(), dialog.phraseWholeWord.isChecked) + } + .setNeutralButton(android.R.string.cancel, null) + .create() + dialog.show() + dialog.phraseWholeWord.isChecked = true + } + + private fun setupEditDialogForItem(itemIndex: Int) { + dialog = AlertDialog.Builder(this@FiltersActivity) + .setTitle(R.string.filter_edit_dialog_title) + .setView(R.layout.dialog_filter) + .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) + 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 + } + + 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) } + } + + private fun loadFilters() { + + filterMessageView.hide() + filtersView.hide() + addFilterButton.hide() + filterProgressBar.show() + + api.getFilters().enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + val filterResponse = response.body() + if(response.isSuccessful && filterResponse != null) { + + filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList() + refreshFilterDisplay() + + filtersView.show() + addFilterButton.show() + filterProgressBar.hide() + } else { + filterProgressBar.hide() + filterMessageView.show() + filterMessageView.setup(R.drawable.elephant_error, + R.string.error_generic) { loadFilters() } + } + } + + override fun onFailure(call: Call>, t: Throwable) { + filterProgressBar.hide() + filterMessageView.show() + if (t is IOException) { + filterMessageView.setup(R.drawable.elephant_offline, + R.string.error_network) { loadFilters() } + } else { + 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() + } + + private fun setupToolbarBackArrow() { + setSupportActionBar(toolbar) + supportActionBar?.run { + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + } + + // Activate back arrow in toolbar + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt new file mode 100644 index 0000000..915baf9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -0,0 +1,84 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.os.Bundle +import androidx.annotation.RawRes +import android.util.Log +import android.view.MenuItem +import android.widget.TextView +import com.keylesspalace.tusky.util.IOUtils +import kotlinx.android.extensions.CacheImplementation +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) + + setSupportActionBar(toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + setTitle(R.string.title_licenses) + + loadFileIntoTextView(R.raw.apache, licenseApacheTextView) + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { + + val sb = StringBuilder() + + val br = BufferedReader(InputStreamReader(resources.openRawResource(fileId))) + + try { + var line: String? = br.readLine() + while (line != null) { + sb.append(line) + sb.append('\n') + line = br.readLine() + } + } catch (e: IOException) { + Log.w("LicenseActivity", e) + } + + 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 new file mode 100644 index 0000000..f5e7641 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -0,0 +1,287 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky + +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.* +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.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewmodel.ListsViewModel +import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.* +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.color +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.utils.toIconicsColor +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +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 + +/** + * Created by charlag on 1/4/18. + */ + +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 adapter = ListsAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_lists) + + + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = getString(R.string.title_lists) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + listsRecycler.adapter = adapter + listsRecycler.layoutManager = LinearLayoutManager(this) + 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 { + showlistNameDialog(null) + } + + viewModel.events.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { event -> + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") + when (event) { + CREATE_ERROR -> showMessage(R.string.error_create_list) + RENAME_ERROR -> showMessage(R.string.error_rename_list) + DELETE_ERROR -> showMessage(R.string.error_delete_list) + } + } + } + + private fun showlistNameDialog(list: MastoList?) { + val layout = FrameLayout(this) + val editText = EditText(this) + editText.setHint(R.string.hint_list_name) + layout.addView(editText) + val margin = Utils.dpToPx(this, 8) + (editText.layoutParams as ViewGroup.MarginLayoutParams) + .setMargins(margin, margin, margin, 0) + + val dialog = AlertDialog.Builder(this) + .setView(layout) + .setPositiveButton( + if (list == null) R.string.action_create_list + else R.string.action_rename_list) { _, _ -> + onPickedDialogName(editText.text, list?.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + + val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) + editText.onTextChanged { s, _, _, _ -> + positiveButton.isEnabled = !s.isNullOrBlank() + } + editText.setText(list?.title) + editText.text?.let { editText.setSelection(it.length) } + } + + private fun showListDeleteDialog(list: MastoList) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) + .setPositiveButton(R.string.action_delete){ _, _ -> + viewModel.deleteList(list.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + + private fun update(state: ListsViewModel.State) { + adapter.submitList(state.lists) + progressBar.visible(state.loadingState == LOADING) + when (state.loadingState) { + INITIAL, LOADING -> messageView.hide() + ERROR_NETWORK -> { + messageView.show() + 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) { + viewModel.retryLoading() + } + } + LOADED -> + if (state.lists.isEmpty()) { + messageView.show() + messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, + null) + } else { + messageView.hide() + } + } + } + + private fun showMessage(@StringRes messageId: Int) { + Snackbar.make( + listsRecycler, messageId, Snackbar.LENGTH_SHORT + ).show() + + } + + private fun onListSelected(listId: String) { + startActivityWithSlideInAnimation( + ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId)) + } + + private fun openListSettings(list: MastoList) { + AccountsInListFragment.newInstance(list.id, list.title).show(supportFragmentManager, null) + } + + private fun renameListDialog(list: MastoList) { + showlistNameDialog(list) + } + + private fun onMore(list: MastoList, view: View) { + PopupMenu(view.context, view).apply { + inflate(R.menu.list_actions) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.list_edit -> openListSettings(list) + R.id.list_rename -> renameListDialog(list) + R.id.list_delete -> showListDeleteDialog(list) + else -> return@setOnMenuItemClickListener false + } + true + } + show() + } + } + + override fun androidInjector() = dispatchingAndroidInjector + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return false + } + + private object ListsDiffer : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { + return oldItem == newItem + } + } + + private inner class ListsAdapter + : ListAdapter(ListsDiffer) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { + return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) + .let(this::ListViewHolder) + .apply { + val context = nameTextView.context + val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary) + val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } + + nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) + } + } + + override fun onBindViewHolder(holder: ListViewHolder, position: Int) { + holder.nameTextView.text = getItem(position).title + } + + private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view), + View.OnClickListener { + val nameTextView: TextView = view.findViewById(R.id.list_name_textview) + val moreButton: ImageButton = view.findViewById(R.id.editListButton) + + init { + view.setOnClickListener(this) + moreButton.setOnClickListener(this) + } + + override fun onClick(v: View) { + if (v == itemView) { + onListSelected(getItem(adapterPosition).id) + } else { + onMore(getItem(adapterPosition), v) + } + } + } + } + + private fun onPickedDialogName(name: CharSequence, listId: String?) { + if (listId == null) { + viewModel.createNewList(name.toString()) + } else { + viewModel.renameList(listId, name.toString()) + } + } +} \ 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 new file mode 100644 index 0000000..92355f5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -0,0 +1,389 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.di.Injectable +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 kotlinx.android.synthetic.main.activity_login.* +import okhttp3.HttpUrl +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import javax.inject.Inject + +class LoginActivity : BaseActivity(), Injectable { + + @Inject + lateinit var mastodonApi: MastodonApi + + private lateinit var preferences: SharedPreferences + + private val oauthRedirectUri: String + get() { + val scheme = getString(R.string.oauth_scheme) + val host = BuildConfig.APPLICATION_ID + return "$scheme://$host/" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_login) + + if(savedInstanceState == null ) { + if(BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { + domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) + domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) + } + appNameEditText.setText(getString(R.string.app_name)) + appNameEditText.setSelection(getString(R.string.app_name).length) + + websiteEditText.setText(getString(R.string.tusky_website)) + websiteEditText.setSelection(getString(R.string.tusky_website).length) + } + + if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { + Glide.with(loginLogo) + .load(BuildConfig.CUSTOM_LOGO_URL) + .placeholder(null) + .into(loginLogo) + } + + preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE) + + loginButton.setOnClickListener { onButtonClick() } + settingsButton.setOnClickListener { onSettingsButtonClick() } + + whatsAnInstanceTextView.setOnClickListener { + val dialog = AlertDialog.Builder(this) + .setMessage(R.string.dialog_whats_an_instance) + .setPositiveButton(R.string.action_close, null) + .show() + val textView = dialog.findViewById(android.R.id.message) + textView?.movementMethod = LinkMovementMethod.getInstance() + } + + if (isAdditionalLogin()) { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) + } else { + toolbar.visibility = View.GONE + } + + } + + override fun requiresLogin(): Boolean { + return false + } + + override fun finish() { + super.finish() + if(isAdditionalLogin()) { + overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun onSettingsButtonClick() { + if(extendedSettings.visibility == View.GONE) { + extendedSettings.visibility = View.VISIBLE + } else { + extendedSettings.visibility = View.GONE + } + + } + + /** + * Obtain the oauth client credentials for this app. This is only necessary the first time the + * app is run on a given server instance. So, after the first authentication, they are + * saved in SharedPreferences and every subsequent run they are simply fetched from there. + */ + private fun onButtonClick() { + + loginButton.isEnabled = false + + val domain = canonicalizeDomain(domainEditText.text.toString()) + + try { + HttpUrl.Builder().host(domain).scheme("https").build() + } catch (e: IllegalArgumentException) { + setLoading(false) + domainTextInputLayout.error = getString(R.string.error_invalid_domain) + return + } + + val callback = object : Callback { + override fun onResponse(call: Call, + response: Response) { + if (!response.isSuccessful) { + loginButton.isEnabled = true + domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, "App authentication failed. " + response.message()) + return + } + val credentials = response.body() + val clientId = credentials!!.clientId + val clientSecret = credentials.clientSecret + + preferences.edit() + .putString("domain", domain) + .putString("clientId", clientId) + .putString("clientSecret", clientSecret) + .apply() + + redirectUserToAuthorizeAndLogin(domain, clientId) + } + + override fun onFailure(call: Call, t: Throwable) { + loginButton.isEnabled = true + domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, Log.getStackTraceString(t)) + } + } + + var appname = getString(R.string.app_name) + var website = getString(R.string.tusky_website) + if(extendedSettings.visibility == View.VISIBLE) { + appname = appNameEditText.text.toString() + website = websiteEditText.text.toString() + } + + mastodonApi + .authenticateApp(domain, appname, oauthRedirectUri, + OAUTH_SCOPES, website) + .enqueue(callback) + setLoading(true) + + } + + private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { + /* To authorize this app and log in it's necessary to redirect to the domain given, + * login there, and the server will redirect back to the app with its response. */ + val endpoint = MastodonApi.ENDPOINT_AUTHORIZE + val parameters = mapOf( + "client_id" to clientId, + "redirect_uri" to oauthRedirectUri, + "response_type" to "code", + "scope" to OAUTH_SCOPES + ) + val url = "https://" + domain + endpoint + "?" + toQueryString(parameters) + val uri = Uri.parse(url) + if (!openInCustomTab(uri, this)) { + val viewIntent = Intent(Intent.ACTION_VIEW, uri) + if (viewIntent.resolveActivity(packageManager) != null) { + startActivity(viewIntent) + } else { + domainEditText.error = getString(R.string.error_no_web_browser_found) + setLoading(false) + } + } + } + + override fun onStart() { + super.onStart() + /* Check if we are resuming during authorization by seeing if the intent contains the + * redirect that was given to the server. If so, its response is here! */ + val uri = intent.data + val redirectUri = oauthRedirectUri + + if (uri != null && uri.toString().startsWith(redirectUri)) { + // This should either have returned an authorization code or an error. + val code = uri.getQueryParameter("code") + val error = uri.getQueryParameter("error") + + /* restore variables from SharedPreferences */ + val domain = preferences.getNonNullString(DOMAIN, "") + val clientId = preferences.getNonNullString(CLIENT_ID, "") + val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "") + + if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) { + + setLoading(true) + /* Since authorization has succeeded, the final step to log in is to exchange + * the authorization code for an access token. */ + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onLoginSuccess(response.body()!!.accessToken, domain) + } else { + setLoading(false) + 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())) + } + } + + override fun onFailure(call: Call, t: Throwable) { + setLoading(false) + 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)) + } + } + + mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code, + "authorization_code").enqueue(callback) + } else if (error != null) { + /* Authorization failed. Put the error response where the user can read it and they + * can try again. */ + setLoading(false) + 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) + } + } else { + // first show or user cancelled login + setLoading(false) + } + } + + private fun setLoading(loadingState: Boolean) { + if (loadingState) { + loginLoadingLayout.visibility = View.VISIBLE + loginInputLayout.visibility = View.GONE + } else { + loginLoadingLayout.visibility = View.GONE + loginInputLayout.visibility = View.VISIBLE + loginButton.isEnabled = true + } + } + + private fun isAdditionalLogin(): Boolean { + return intent.getBooleanExtra(LOGIN_MODE, false) + } + + private fun onLoginSuccess(accessToken: String, domain: String) { + + setLoading(true) + + accountManager.addAccount(accessToken, domain) + + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + overridePendingTransition(R.anim.explode, R.anim.explode) + } + + companion object { + private const val TAG = "LoginActivity" // logging tag + private const val OAUTH_SCOPES = "read write follow" + private const val LOGIN_MODE = "LOGIN_MODE" + private const val DOMAIN = "domain" + private const val CLIENT_ID = "clientId" + private const val CLIENT_SECRET = "clientSecret" + + @JvmStatic + fun getIntent(context: Context, mode: Boolean): Intent { + val loginIntent = Intent(context, LoginActivity::class.java) + loginIntent.putExtra(LOGIN_MODE, mode) + return loginIntent + } + + /** Make sure the user-entered text is just a fully-qualified domain name. */ + private fun canonicalizeDomain(domain: String): String { + // Strip any schemes out. + var s = domain.replaceFirst("http://", "") + s = s.replaceFirst("https://", "") + // If a username was included (e.g. username@example.com), just take what's after the '@'. + val at = s.lastIndexOf('@') + if (at != -1) { + s = s.substring(at + 1) + } + return s.trim { it <= ' ' } + } + + /** + * Chain together the key-value pairs into a query string, for either appending to a URL or + * as the content of an HTTP request. + */ + private fun toQueryString(parameters: Map): String { + val s = StringBuilder() + var between = "" + for ((key, value) in parameters) { + s.append(between) + s.append(Uri.encode(key)) + s.append("=") + s.append(Uri.encode(value)) + between = "&" + } + return s.toString() + } + + private fun openInCustomTab(uri: Uri, context: Context): Boolean { + + val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) + val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) + val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) + + val colorSchemeParams = CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build() + + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .build() + + try { + customTabsIntent.launchUrl(context, uri) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "Activity was not found for intent $customTabsIntent") + return false + } + + return true + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt new file mode 100644 index 0000000..dd0dd79 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -0,0 +1,836 @@ +/* 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 + +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.KeyEvent +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.emoji.text.EmojiCompat +import androidx.emoji.text.EmojiCompat.InitCallback +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.viewpager2.widget.MarginPageTransformer +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.FixedSizeDrawable +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import com.google.android.material.tabs.TabLayoutMediator +import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType +import com.keylesspalace.tusky.components.conversation.ConversationsRepository +import com.keylesspalace.tusky.components.drafts.DraftsActivity +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.preference.PreferencesActivity +import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity +import com.keylesspalace.tusky.components.search.SearchActivity +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.pager.MainPagerAdapter +import com.keylesspalace.tusky.service.StreamingService +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +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 com.mikepenz.materialdrawer.holder.BadgeStyle +import com.mikepenz.materialdrawer.holder.ColorHolder +import com.mikepenz.materialdrawer.holder.StringHolder +import com.mikepenz.materialdrawer.iconics.iconicsIcon +import com.mikepenz.materialdrawer.model.* +import com.mikepenz.materialdrawer.model.interfaces.* +import com.mikepenz.materialdrawer.util.* +import com.mikepenz.materialdrawer.widget.AccountHeaderView +import com.uber.autodispose.android.lifecycle.autoDispose +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 retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject + +class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var cacheUpdater: CacheUpdater + + @Inject + lateinit var conversationRepository: ConversationsRepository + + @Inject + lateinit var appDb: AppDatabase + + private lateinit var header: AccountHeaderView + + private var notificationTabPosition = 0 + private var onTabSelectedListener: OnTabSelectedListener? = null + + private var unreadAnnouncementsCount = 0 + + private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + + private lateinit var glide: RequestManager + + private val emojiInitCallback = object : InitCallback() { + override fun onInitialized() { + if (!isDestroyed) { + updateProfiles() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { + // will be redirected to LoginActivity by BaseActivity + return + } + var showNotificationTab = false + if (intent != null) { + /** there are two possibilities the accountId can be passed to MainActivity: + * - from our code as long 'account_id' + * - from share shortcuts as String 'android.intent.extra.shortcut.ID' + */ + var accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1) + if (accountId == -1L) { + val accountIdString = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID) + if (accountIdString != null) { + accountId = accountIdString.toLong() + } + } + val accountRequested = accountId != -1L + if (accountRequested && accountId != activeAccount.id) { + accountManager.setActiveAccount(accountId) + } + if (canHandleMimeType(intent.type)) { + // Sharing to Tusky from an external app + if (accountRequested) { + // The correct account is already active + forwardShare(intent) + } else { + // No account was provided, show the chooser + showAccountChooserDialog(getString(R.string.action_share_as), true, object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + val requestedId = account.id + if (requestedId == activeAccount.id) { + // The correct account is already active + forwardShare(intent) + } else { + // A different account was requested, restart the activity + intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId) + changeAccount(requestedId, intent) + } + } + }) + } + } else if (accountRequested) { + // user clicked a notification, show notification tab and switch user if necessary + showNotificationTab = true + } + } + window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own + setContentView(R.layout.activity_main) + ViewPager2Fix.reduceVelocity(viewPager, 2.0f); + + glide = Glide.with(this) + + composeButton.setOnClickListener { + val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + startActivity(composeIntent) + } + + val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) + mainToolbar.visible(!hideTopToolbar) + + loadDrawerAvatar(activeAccount.profilePictureUrl, true) + + 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 + colorInt = ThemeUtils.getColor(this@MainActivity, android.R.attr.textColorPrimary) + } + setOnMenuItemClickListener { + startActivity(SearchActivity.getIntent(this@MainActivity)) + true + } + } + + setupDrawer(savedInstanceState, addSearchButton = hideTopToolbar) + + /* Fetch user info while we're doing other things. This has to be done after setting up the + * drawer, though, because its callback touches the header in the drawer. */ + fetchUserInfo() + + fetchAnnouncements() + + setupTabs(showNotificationTab) + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when (event) { + is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) + is MainTabsChangedEvent -> setupTabs(false) + is PreferenceChangedEvent -> { + when (event.preferenceKey) { + PrefKeys.LIVE_NOTIFICATIONS -> { + initPullNotifications() + } + } + } + is AnnouncementReadEvent -> { + unreadAnnouncementsCount-- + updateAnnouncementsBadge() + } + } + } + + Schedulers.io().scheduleDirect { + // Flush old media that was cached for sharing + deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Husky")) + } + } + + private fun initPullNotifications() { + if (NotificationHelper.areNotificationsEnabled(this, accountManager)) { + if(accountManager.areNotificationsStreamingEnabled()) { + StreamingService.startStreaming(this) + NotificationHelper.disablePullNotifications(this) + } else { + StreamingService.stopStreaming(this) + NotificationHelper.enablePullNotifications(this) + } + } else { + StreamingService.stopStreaming(this) + NotificationHelper.disablePullNotifications(this) + } + draftWarning() + } + + override fun onResume() { + super.onResume() + NotificationHelper.clearNotificationsForActiveAccount(this, accountManager) + } + + override fun onBackPressed() { + when { + mainDrawerLayout.isOpen -> { + mainDrawerLayout.close() + } + viewPager.currentItem != 0 -> { + viewPager.currentItem = 0 + } + else -> { + super.onBackPressed() + } + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_MENU -> { + if (mainDrawerLayout.isOpen) { + mainDrawerLayout.close() + } else { + mainDrawerLayout.open() + } + return true + } + KeyEvent.KEYCODE_SEARCH -> { + startActivityWithSlideInAnimation(SearchActivity.getIntent(this)) + return true + } + } + if (event.isCtrlPressed || event.isShiftPressed) { + // FIXME: blackberry keyONE raises SHIFT key event even CTRL IS PRESSED + when (keyCode) { + KeyEvent.KEYCODE_N -> { + + // open compose activity by pressing SHIFT + N (or CTRL + N) + val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + startActivity(composeIntent) + return true + } + } + } + return super.onKeyDown(keyCode, event) + } + + public override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + if (intent != null) { + val statusUrl = intent.getStringExtra(STATUS_URL) + if (statusUrl != null) { + viewUrl(statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR) + } + } + } + + override fun onDestroy() { + super.onDestroy() + EmojiCompat.get().unregisterInitCallback(emojiInitCallback) + } + + private fun forwardShare(intent: Intent) { + val composeIntent = Intent(this, ComposeActivity::class.java) + composeIntent.action = intent.action + composeIntent.type = intent.type + composeIntent.putExtras(intent) + composeIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(composeIntent) + finish() + } + + private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { + + mainToolbar.setNavigationOnClickListener { + mainDrawerLayout.open() + } + + header = AccountHeaderView(this).apply { + headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP + currentHiddenInList = true + onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) } + addProfile(ProfileSettingDrawerItem().apply { + identifier = DRAWER_ITEM_ADD_ACCOUNT + nameRes = R.string.add_account_name + descriptionRes = R.string.add_account_description + iconicsIcon = GoogleMaterial.Icon.gmd_add + }, 0) + attachToSliderView(mainDrawer) + dividerBelowHeader = false + closeDrawerOnProfileListClick = true + } + + header.accountHeaderBackground.setColorFilter(ContextCompat.getColor(this, R.color.headerBackgroundFilter)) + header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent)) + val animateAvatars = preferences.getBoolean("animateGifAvatars", false) + + DrawerImageLoader.init(object : AbstractDrawerImageLoader() { + override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { + if (animateAvatars) { + glide.load(uri) + .placeholder(placeholder) + .into(imageView) + } else { + glide.asBitmap() + .load(uri) + .placeholder(placeholder) + .into(imageView) + } + } + + override fun cancel(imageView: ImageView) { + glide.clear(imageView) + } + + override fun placeholder(ctx: Context, tag: String?): Drawable { + if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) { + return ctx.getDrawable(R.drawable.avatar_default)!! + } + + return super.placeholder(ctx, tag) + } + }) + + mainDrawer.apply { + tintStatusBar = true + addItems( + primaryDrawerItem { + nameRes = R.string.action_edit_profile + iconicsIcon = GoogleMaterial.Icon.gmd_person + onClick = { + val intent = Intent(context, EditProfileActivity::class.java) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_favourites + isSelectable = false + iconicsIcon = GoogleMaterial.Icon.gmd_star + onClick = { + val intent = StatusListActivity.newFavouritesIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_bookmarks + iconicsIcon = GoogleMaterial.Icon.gmd_bookmark + onClick = { + val intent = StatusListActivity.newBookmarksIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_lists + iconicsIcon = GoogleMaterial.Icon.gmd_list + onClick = { + startActivityWithSlideInAnimation(ListsActivity.newIntent(context)) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_saved_toot + iconRes = R.drawable.ic_notebook + onClick = { + val intent = DraftsActivity.newIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_scheduled_toot + iconRes = R.drawable.ic_access_time + onClick = { + startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context)) + } + }, + primaryDrawerItem { + identifier = DRAWER_ITEM_ANNOUNCEMENTS + nameRes = R.string.title_announcements + iconRes = R.drawable.ic_bullhorn_24dp + onClick = { + startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) + } + badgeStyle = BadgeStyle().apply { + textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary)) + color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary)) + } + }, + DividerDrawerItem(), + secondaryDrawerItem { + nameRes = R.string.action_view_account_preferences + iconRes = R.drawable.ic_account_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_view_preferences + iconicsIcon = GoogleMaterial.Icon.gmd_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.about_title_activity + iconicsIcon = GoogleMaterial.Icon.gmd_info + onClick = { + val intent = Intent(context, AboutActivity::class.java) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_logout + iconRes = R.drawable.ic_logout + onClick = ::logout + } + ) + + if (addSearchButton) { + mainDrawer.addItemsAtPosition(4, + primaryDrawerItem { + nameRes = R.string.action_search + iconicsIcon = GoogleMaterial.Icon.gmd_search + onClick = { + startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) + } + }) + } + + setSavedInstance(savedInstanceState) + } + + if (BuildConfig.DEBUG) { + mainDrawer.addItems( + secondaryDrawerItem { + nameText = "debug" + isEnabled = false + textColor = ColorStateList.valueOf(Color.GREEN) + } + ) + } + EmojiCompat.get().registerInitCallback(emojiInitCallback) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) + } + + private fun setupTabs(selectNotificationTab: Boolean) { + + 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 + } else { + bottomNav.hide() + (viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 + (composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager + tabLayout + } + + val tabs = accountManager.activeAccount!!.tabPreferences + + val adapter = MainPagerAdapter(tabs, this) + viewPager.adapter = adapter + TabLayoutMediator(activeTabLayout, viewPager, TabConfigurationStrategy { _: TabLayout.Tab?, _: Int -> }).attach() + activeTabLayout.removeAllTabs() + for (i in tabs.indices) { + val tab = activeTabLayout.newTab() + .setIcon(tabs[i].icon) + if (tabs[i].id == LIST) { + tab.contentDescription = tabs[i].arguments[1] + } else { + tab.setContentDescription(tabs[i].text) + } + activeTabLayout.addTab(tab) + + if (tabs[i].id == NOTIFICATIONS) { + notificationTabPosition = i + if (selectNotificationTab) { + tab.select() + } + } + } + + val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) + viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + + val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true) + viewPager.isUserInputEnabled = enableSwipeForTabs + + onTabSelectedListener?.let { + activeTabLayout.removeOnTabSelectedListener(it) + } + + onTabSelectedListener = object : OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + if (tab.position == notificationTabPosition) { + NotificationHelper.clearNotificationsForActiveAccount(this@MainActivity, accountManager) + } + + mainToolbar.title = tabs[tab.position].title(this@MainActivity) + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + + override fun onTabReselected(tab: TabLayout.Tab) { + val fragment = adapter.getFragment(tab.position) + if (fragment is ReselectableFragment) { + (fragment as ReselectableFragment).onReselect() + } + } + }.also { + activeTabLayout.addOnTabSelectedListener(it) + } + + val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 + mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) + mainToolbar.setOnClickListener { + (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() + } + + } + + private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean { + val activeAccount = accountManager.activeAccount + + //open profile when active image was clicked + if (current && activeAccount != null) { + val intent = AccountActivity.getIntent(this, activeAccount.accountId) + startActivityWithSlideInAnimation(intent) + return false + } + //open LoginActivity to add new account + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true)) + return false + } + //change Account + changeAccount(profile.identifier, null) + return false + } + + private fun changeAccount(newSelectedId: Long, forward: Intent?) { + cacheUpdater.stop() + SFragment.flushFilters() + accountManager.setActiveAccount(newSelectedId) + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + if (forward != null) { + intent.type = forward.type + intent.action = forward.action + intent.putExtras(forward) + } + startActivity(intent) + finishWithoutSlideOutAnimation() + overridePendingTransition(R.anim.explode, R.anim.explode) + } + + private fun logout() { + accountManager.activeAccount?.let { activeAccount -> + AlertDialog.Builder(this) + .setTitle(R.string.action_logout) + .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) + .setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int -> + NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) + cacheUpdater.clearForUser(activeAccount.id) + conversationRepository.deleteCacheForAccount(activeAccount.id) + removeShortcut(this, activeAccount) + val newAccount = accountManager.logActiveAccountOut() + initPullNotifications() + val intent = if (newAccount == null) { + LoginActivity.getIntent(this, false) + } else { + Intent(this, MainActivity::class.java) + } + startActivity(intent) + finishWithoutSlideOutAnimation() + } + .setNegativeButton(android.R.string.no, null) + .show() + } + } + + private fun fetchUserInfo() { + mastodonApi.accountVerifyCredentials() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) + } + ) + } + + private fun onFetchUserInfoSuccess(me: Account) { + glide.asBitmap() + .load(me.header) + .into(header.accountHeaderBackground) + + loadDrawerAvatar(me.avatar, false) + + accountManager.updateActiveAccount(me) + NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) + + initPullNotifications() + + // Show follow requests in the menu, if this is a locked account. + if (me.locked && 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) + } + } + mainDrawer.addItemAtPosition(4, followRequestsItem) + } else if (!me.locked) { + mainDrawer.removeItems(DRAWER_ITEM_FOLLOW_REQUESTS) + } + updateProfiles() + updateShortcut(this, accountManager.activeAccount!!) + } + + private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { + val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) + + glide.asDrawable() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) { + placeholder(R.drawable.avatar_default) + } + } + .into(object : CustomTarget(navIconSize, navIconSize) { + + override fun onLoadStarted(placeholder: Drawable?) { + if(placeholder != null) { + mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + override fun onResourceReady(resource: Drawable, transition: Transition?) { + mainToolbar.navigationIcon = resource + } + + override fun onLoadCleared(placeholder: Drawable?) { + mainToolbar.navigationIcon = placeholder + } + }) + } + + private fun fetchAnnouncements() { + mastodonApi.listAnnouncements(false) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { + Log.w(TAG, "Failed to fetch announcements.", it) + } + ) + } + + private fun updateAnnouncementsBadge() { + mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) + } + + private fun updateProfiles() { + val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, true)) + + ProfileDrawerItem().apply { + isSelected = acc.isActive + nameText = emojifiedName + iconUrl = acc.profilePictureUrl + isNameShown = true + identifier = acc.id + descriptionText = acc.fullName + } + }.toMutableList() + + // reuse the already existing "add account" item + for (profile in header.profiles.orEmpty()) { + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + profiles.add(profile) + break + } + } + header.clear() + header.profiles = profiles + header.setActiveProfile(accountManager.activeAccount!!.id) + } + + private fun draftWarning() { + val sharedPrefsKey = "show_draft_warning" + appDb.tootDao().savedTootCount() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { draftCount -> + val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true) + if (draftCount > 0 && showDraftWarning) { + AlertDialog.Builder(this) + .setMessage(R.string.new_drafts_warning) + .setNegativeButton("Don't show again") { _, _ -> + preferences.edit(commit = true) { + putBoolean(sharedPrefsKey, false) + } + } + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + + } + + override fun getActionButton(): FloatingActionButton? = composeButton + + override fun androidInjector() = androidInjector + + 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" + } +} + +private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { + return PrimaryDrawerItem() + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) +} + +private inline fun secondaryDrawerItem(block: SecondaryDrawerItem.() -> Unit): SecondaryDrawerItem { + return SecondaryDrawerItem() + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) +} + +private var AbstractDrawerItem<*, *>.onClick: () -> Unit + get() = throw UnsupportedOperationException() + set(value) { + onDrawerItemClickListener = { _, _, _ -> + value() + false + } + } diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt new file mode 100644 index 0000000..e4655a5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -0,0 +1,69 @@ +package com.keylesspalace.tusky + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import com.google.android.material.floatingactionbutton.FloatingActionButton +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) + + setSupportActionBar(toolbar) + val bar = supportActionBar + if (bar != null) { + bar.title = getString(R.string.title_list_timeline) + bar.setDisplayHomeAsUpEnabled(true) + bar.setDisplayShowHomeEnabled(true) + } + + if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { + val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind + ?: TimelineFragment.Kind.HOME + val argument = intent?.getStringExtra(ARG_ARG) + supportFragmentManager.beginTransaction() + .replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument)) + .commit() + } + } + + override fun getActionButton(): FloatingActionButton? = null + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return false + } + + override fun androidInjector() = dispatchingAndroidInjector + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java new file mode 100644 index 0000000..8e6b580 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -0,0 +1,222 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky; + +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.Lifecycle; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.keylesspalace.tusky.adapter.SavedTootAdapter; +import com.keylesspalace.tusky.appstore.EventHub; +import com.keylesspalace.tusky.appstore.StatusComposedEvent; +import com.keylesspalace.tusky.components.compose.ComposeActivity; +import com.keylesspalace.tusky.db.AppDatabase; +import com.keylesspalace.tusky.db.TootDao; +import com.keylesspalace.tusky.db.TootEntity; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.util.SaveTootHelper; +import com.keylesspalace.tusky.view.BackgroundMessageView; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + +public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction, + Injectable { + + // ui + private SavedTootAdapter adapter; + private BackgroundMessageView errorMessageView; + + private List toots = new ArrayList<>(); + @Nullable + private AsyncTask asyncTask; + + @Inject + EventHub eventHub; + @Inject + AppDatabase database; + @Inject + SaveTootHelper saveTootHelper; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .ofType(StatusComposedEvent.class) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe((__) -> this.fetchToots()); + + setContentView(R.layout.activity_saved_toot); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setTitle(getString(R.string.title_drafts)); + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); + } + + RecyclerView recyclerView = findViewById(R.id.recyclerView); + errorMessageView = findViewById(R.id.errorMessageView); + recyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + DividerItemDecoration divider = new DividerItemDecoration( + this, layoutManager.getOrientation()); + recyclerView.addItemDecoration(divider); + adapter = new SavedTootAdapter(this); + recyclerView.setAdapter(adapter); + } + + @Override + protected void onResume() { + super.onResume(); + fetchToots(); + } + + @Override + protected void onPause() { + super.onPause(); + if (asyncTask != null) asyncTask.cancel(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + private void fetchToots() { + asyncTask = new FetchPojosTask(this, database.tootDao()) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void setNoContent(int size) { + if (size == 0) { + errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status, null); + errorMessageView.setVisibility(View.VISIBLE); + } else { + errorMessageView.setVisibility(View.GONE); + } + } + + @Override + public void delete(int position, TootEntity item) { + + saveTootHelper.deleteDraft(item); + + toots.remove(position); + // update adapter + if (adapter != null) { + adapter.removeItem(position); + setNoContent(toots.size()); + } + } + + @Override + public void click(int position, TootEntity item) { + Gson gson = new Gson(); + Type stringListType = new TypeToken>() {}.getType(); + List jsonUrls = gson.fromJson(item.getUrls(), stringListType); + List descriptions = gson.fromJson(item.getDescriptions(), stringListType); + + ComposeOptions composeOptions = new ComposeOptions( + /*scheduledTootUid*/null, + item.getUid(), + /*drafId*/null, + item.getText(), + jsonUrls, + descriptions, + /*mentionedUsernames*/null, + item.getInReplyToId(), + /*replyVisibility*/null, + item.getVisibility(), + item.getContentWarning(), + item.getInReplyToUsername(), + item.getInReplyToText(), + /*mediaAttachments*/null, + /*draftAttachments*/null, + /*scheduledAt*/null, + /*sensitive*/null, + /*poll*/null, + item.getFormattingSyntax(), + /* modifiedInitialState */ true + ); + Intent intent = ComposeActivity.startIntent(this, composeOptions); + startActivity(intent); + } + + static final class FetchPojosTask extends AsyncTask> { + + private final WeakReference activityRef; + private final TootDao tootDao; + + FetchPojosTask(SavedTootActivity activity, TootDao tootDao) { + this.activityRef = new WeakReference<>(activity); + this.tootDao = tootDao; + } + + @Override + protected List doInBackground(Void... voids) { + return tootDao.loadAll(); + } + + @Override + protected void onPostExecute(List pojos) { + super.onPostExecute(pojos); + SavedTootActivity activity = activityRef.get(); + if (activity == null) return; + + activity.toots.clear(); + activity.toots.addAll(pojos); + + // set ui + activity.setNoContent(pojos.size()); + activity.adapter.setItems(activity.toots); + activity.adapter.notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt new file mode 100644 index 0000000..07c54e9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt @@ -0,0 +1,50 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable + +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import javax.inject.Inject + +class SplashActivity : AppCompatActivity(), Injectable { + + @Inject + lateinit var accountManager: AccountManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + /** delete old notification channels */ + NotificationHelper.deleteLegacyNotificationChannels(this, accountManager) + + /** Determine whether the user is currently logged in, and if so go ahead and load the + * timeline. Otherwise, start the activity_login screen. */ + + val intent = if (accountManager.activeAccount != null) { + Intent(this, MainActivity::class.java) + } else { + LoginActivity.getIntent(this, false) + } + startActivity(intent) + finish() + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt new file mode 100644 index 0000000..56ea4d2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -0,0 +1,96 @@ +/* Copyright 2019 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 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.fragment.app.commit + +import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.fragment.TimelineFragment.Kind + +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 { + + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + + 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) + + setSupportActionBar(toolbar) + + val title = if(kind == Kind.FAVOURITES) { + R.string.title_favourites + } else { + R.string.title_bookmarks + } + + supportActionBar?.run { + setTitle(title) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager.commit { + val fragment = TimelineFragment.newInstance(kind) + replace(R.id.fragment_container, fragment) + } + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home){ + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + + private const val EXTRA_KIND = "kind" + + @JvmStatic + fun newFavouritesIntent(context: Context) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.FAVOURITES.name) + } + + @JvmStatic + fun newBookmarksIntent(context: Context) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.BOOKMARKS.name) + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt new file mode 100644 index 0000000..cf74670 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -0,0 +1,112 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.keylesspalace.tusky.components.conversation.ConversationsFragment +import com.keylesspalace.tusky.fragment.ChatsFragment +import com.keylesspalace.tusky.fragment.NotificationsFragment +import com.keylesspalace.tusky.fragment.TimelineFragment + +/** this would be a good case for a sealed class, but that does not work nice with Room */ + +const val HOME = "Home" +const val NOTIFICATIONS = "Notifications" +const val LOCAL = "Local" +const val FEDERATED = "Federated" +const val DIRECT = "Direct" +const val HASHTAG = "Hashtag" +const val LIST = "List" +const val CHATS = "Chats" + +data class TabData(val id: String, + @StringRes val text: Int, + @DrawableRes val icon: Int, + val fragment: (List) -> Fragment, + val arguments: List = emptyList(), + val title: (Context) -> String = { context -> context.getString(text)} + ) + +fun createTabDataFromId(id: String, arguments: List = emptyList()): TabData { + return when (id) { + HOME -> TabData( + HOME, + R.string.title_home, + R.drawable.ic_home_24dp, + { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) } + ) + NOTIFICATIONS -> TabData( + NOTIFICATIONS, + R.string.title_notifications, + R.drawable.ic_notifications_24dp, + { NotificationsFragment.newInstance() } + ) + LOCAL -> TabData( + LOCAL, + R.string.title_public_local, + R.drawable.ic_local_24dp, + { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) } + ) + FEDERATED -> TabData( + FEDERATED, + R.string.title_public_federated, + R.drawable.ic_public_24dp, + { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) } + ) + DIRECT -> TabData( + DIRECT, + R.string.title_direct_messages, + R.drawable.ic_reblog_direct_24dp, + { ConversationsFragment.newInstance() } + ) + HASHTAG -> TabData( + HASHTAG, + R.string.hashtags, + R.drawable.ic_hashtag, + { args -> TimelineFragment.newHashtagInstance(args) }, + arguments, + { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) }} + ) + LIST -> TabData( + LIST, + R.string.list, + R.drawable.ic_list, + { args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty()) }, + arguments, + { arguments.getOrNull(1).orEmpty() } + ) + CHATS -> TabData( + CHATS, + R.string.chats, + R.drawable.ic_forum_24px, + { ChatsFragment() } + ) + else -> throw IllegalArgumentException("unknown tab type") + } +} + +fun defaultTabs(): List { + return listOf( + createTabDataFromId(HOME), + createTabDataFromId(NOTIFICATIONS), + createTabDataFromId(LOCAL), + createTabDataFromId(FEDERATED), + createTabDataFromId(CHATS) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt new file mode 100644 index 0000000..b5539bb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -0,0 +1,372 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.FrameLayout +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.transition.MaterialArcMotion +import com.google.android.material.transition.MaterialContainerTransform +import com.keylesspalace.tusky.adapter.ItemInteractionListener +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.di.Injectable +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.onTextChanged +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 + +class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener { + + @Inject + lateinit var mastodonApi: MastodonApi + @Inject + lateinit var eventHub: EventHub + + private lateinit var currentTabs: MutableList + private lateinit var currentTabsAdapter: TabAdapter + private lateinit var touchHelper: ItemTouchHelper + private lateinit var addTabAdapter: TabAdapter + + private var tabsChanged = false + + private val selectedItemElevation by lazy { resources.getDimension(R.dimen.selected_drag_item_elevation) } + + private val hashtagRegex by lazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_tab_preference) + + setSupportActionBar(toolbar) + + supportActionBar?.apply { + setTitle(R.string.title_tab_preferences) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + 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)) + + addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this) + addTabRecyclerView.adapter = addTabAdapter + addTabRecyclerView.layoutManager = LinearLayoutManager(this) + + touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END) + } + + override fun isLongPressDragEnabled(): Boolean { + return true + } + + override fun isItemViewSwipeEnabled(): Boolean { + return MIN_TAB_COUNT < currentTabs.size + } + + 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 + + currentTabsAdapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition) + saveTabs() + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + onTabRemoved(viewHolder.adapterPosition) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.elevation = selectedItemElevation + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.elevation = 0f + } + }) + + touchHelper.attachToRecyclerView(currentTabsRecyclerView) + + actionButton.setOnClickListener { + toggleFab(true) + } + + scrim.setOnClickListener { + toggleFab(false) + } + + maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) + + updateAvailableTabs() + } + + override fun onTabAdded(tab: TabData) { + + if (currentTabs.size >= MAX_TAB_COUNT) { + return + } + + toggleFab(false) + + if (tab.id == HASHTAG) { + showAddHashtagDialog() + return + } + + if (tab.id == LIST) { + showSelectListDialog() + return + } + + currentTabs.add(tab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() + } + + override fun onTabRemoved(position: Int) { + currentTabs.removeAt(position) + currentTabsAdapter.notifyItemRemoved(position) + updateAvailableTabs() + saveTabs() + } + + override fun onActionChipClicked(tab: TabData, tabPosition: Int) { + showAddHashtagDialog(tab, tabPosition) + } + + override fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) { + val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition } + val newTab = tab.copy(arguments = newArguments) + currentTabs[tabPosition] = newTab + saveTabs() + + currentTabsAdapter.notifyItemChanged(tabPosition) + } + + private fun toggleFab(expand: Boolean) { + val transition = MaterialContainerTransform().apply { + startView = if (expand) actionButton else sheet + val endView: View = if (expand) sheet else actionButton + this.endView = endView + addTarget(endView) + scrimColor = Color.TRANSPARENT + setPathMotion(MaterialArcMotion()) + } + + TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition) + actionButton.visible(!expand) + sheet.visible(expand) + scrim.visible(expand) + } + + private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { + + val frameLayout = FrameLayout(this) + val padding = Utils.dpToPx(this, 8) + frameLayout.updatePadding(left = padding, right = padding) + + val editText = AppCompatEditText(this) + editText.setHint(R.string.edit_hashtag_hint) + editText.setText("") + frameLayout.addView(editText) + + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.add_hashtag_title) + .setView(frameLayout) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.action_save) { _, _ -> + val input = editText.text.toString().trim() + if (tab == null) { + val newTab = createTabDataFromId(HASHTAG, listOf(input)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + } else { + val newTab = tab.copy(arguments = tab.arguments + input) + currentTabs[tabPosition] = newTab + + currentTabsAdapter.notifyItemChanged(tabPosition) + } + + updateAvailableTabs() + saveTabs() + } + .create() + + editText.onTextChanged { s, _, _, _ -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s) + } + + dialog.show() + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(editText.text) + editText.requestFocus() + } + + private fun showSelectListDialog() { + val adapter = ListSelectionAdapter(this) + mastodonApi.getLists() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe ( + { lists -> + adapter.addAll(lists) + }, + { throwable -> + Log.e("TabPreferenceActivity", "failed to load lists", throwable) + } + ) + + AlertDialog.Builder(this) + .setTitle(R.string.select_list_title) + .setAdapter(adapter) { _, position -> + val list = adapter.getItem(position) + val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() + } + .show() + } + + private fun validateHashtag(input: CharSequence?): Boolean { + val trimmedInput = input?.trim() ?: "" + return trimmedInput.isNotEmpty() && hashtagRegex.matcher(trimmedInput).matches() + } + + private fun updateAvailableTabs() { + val addableTabs: MutableList = mutableListOf() + + val homeTab = createTabDataFromId(HOME) + if (!currentTabs.contains(homeTab)) { + addableTabs.add(homeTab) + } + val notificationTab = createTabDataFromId(NOTIFICATIONS) + if (!currentTabs.contains(notificationTab)) { + addableTabs.add(notificationTab) + } + val localTab = createTabDataFromId(LOCAL) + if (!currentTabs.contains(localTab)) { + addableTabs.add(localTab) + } + val federatedTab = createTabDataFromId(FEDERATED) + if (!currentTabs.contains(federatedTab)) { + addableTabs.add(federatedTab) + } + val directMessagesTab = createTabDataFromId(DIRECT) + if (!currentTabs.contains(directMessagesTab)) { + addableTabs.add(directMessagesTab) + } + val chatTab = createTabDataFromId(CHATS) + if (!currentTabs.contains(chatTab)) { + addableTabs.add(chatTab) + } + + addableTabs.add(createTabDataFromId(HASHTAG)) + addableTabs.add(createTabDataFromId(LIST)) + + addTabAdapter.updateData(addableTabs) + + maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) + currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT); + } + + override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startSwipe(viewHolder) + } + + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startDrag(viewHolder) + } + + private fun saveTabs() { + accountManager.activeAccount?.let { + Single.fromCallable { + it.tabPreferences = currentTabs + accountManager.saveAccount(it) + } + .subscribeOn(Schedulers.io()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe() + + } + tabsChanged = true + } + + override fun onBackPressed() { + if (actionButton.isVisible) { + super.onBackPressed() + } else { + toggleFab(false) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return false + } + + override fun onPause() { + super.onPause() + if (tabsChanged) { + eventHub.dispatch(MainTabsChangedEvent(currentTabs)) + } + } + + companion object { + private const val MIN_TAB_COUNT = 2 + private const val MAX_TAB_COUNT = 9 + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt new file mode 100644 index 0000000..fe5f3b8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -0,0 +1,114 @@ +/* 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 + +import android.app.Application +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.util.Log +import androidx.emoji.text.EmojiCompat +import androidx.preference.PreferenceManager +import androidx.work.WorkManager +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.github.piasy.biv.BigImageViewer +import com.github.piasy.biv.loader.glide.GlideCustomImageLoader +import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory +import com.keylesspalace.tusky.di.AppInjector +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.LocaleManager +import com.keylesspalace.tusky.util.ThemeUtils +import com.uber.autodispose.AutoDisposePlugins +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import io.reactivex.plugins.RxJavaPlugins +import org.conscrypt.Conscrypt +import java.security.Security +import javax.inject.Inject + +class TuskyApplication : Application(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var notificationWorkerFactory: NotificationWorkerFactory + + override fun onCreate() { + // Uncomment me to get StrictMode violation logs +// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { +// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() +// .detectDiskReads() +// .detectDiskWrites() +// .detectNetwork() +// .detectUnbufferedIo() +// .penaltyLog() +// .build()) +// } + super.onCreate() + + Security.insertProviderAt(Conscrypt.newProvider(), 1) + + AutoDisposePlugins.setHideProxies(false) // a small performance optimization + + AppInjector.init(this) + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + + // init the custom emoji fonts + val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) + val emojiConfig = EmojiCompatFont.byId(emojiSelection) + .getConfig(this) + .setReplaceAll(true) + EmojiCompat.init(emojiConfig) + + // init night mode + val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) + ThemeUtils.setAppNightMode(theme) + + RxJavaPlugins.setErrorHandler { + Log.w("RxJava", "undeliverable exception", it) + } + + SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.ARGB_8888) + BigImageViewer.initialize(GlideCustomImageLoader.with(this)) + + WorkManager.initialize( + this, + androidx.work.Configuration.Builder() + .setWorkerFactory(notificationWorkerFactory) + .build() + ) + } + + override fun attachBaseContext(base: Context) { + localeManager = LocaleManager(base) + super.attachBaseContext(localeManager.setLocale(base)) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + localeManager.setLocale(this) + } + + override fun androidInjector() = androidInjector + + companion object { + @JvmStatic + lateinit var localeManager: LocaleManager + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt new file mode 100644 index 0000000..b40adb1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -0,0 +1,388 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.DownloadManager +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.transition.Transition +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +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.entity.Attachment +import com.keylesspalace.tusky.fragment.ViewImageFragment +import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter +import com.keylesspalace.tusky.pager.ImagePagerAdapter +import com.keylesspalace.tusky.util.getTemporaryMediaFilename +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 +import java.io.IOException +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_AVATAR_URL = "avatar" + 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 newIntent(context: Context?, attachment: Attachment): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, + arrayListOf(AttachmentViewData(attachment, null, null))) + intent.putExtra(EXTRA_ATTACHMENT_INDEX, 0) + return intent + } + + fun newSingleImageIntent(context: Context?, url: String): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putExtra(EXTRA_AVATAR_URL, url) + return intent + } + } + + var isToolbarVisible = true + private set + + private var attachments: ArrayList? = null + private val toolbarVisibilityListeners = mutableListOf() + + fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0 { + this.toolbarVisibilityListeners.add(listener) + listener(isToolbarVisible) + return { toolbarVisibilityListeners.remove(listener) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_view_media) + + supportPostponeEnterTransition() + + // Gather the parameters. + attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS) + val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) + + // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener + // but it cannot be expressed and if I don't specify type explicitly compilation fails + // (probably a bug in compiler) + val adapter: ViewMediaAdapter = if (attachments != null) { + val realAttachs = attachments!!.map(AttachmentViewData::attachment) + // Setup the view pager. + ImagePagerAdapter(this, realAttachs, initialPosition) + + } else { + val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL) + ?: throw IllegalArgumentException("attachment list or avatar url has to be set") + + AvatarImagePagerAdapter(this, avatarUrl) + } + + viewPager.adapter = adapter + viewPager.setCurrentItem(initialPosition, false) + viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + toolbar.title = getPageTitle(position) + } + }) + + // Setup the toolbar. + setSupportActionBar(toolbar) + val actionBar = supportActionBar + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true) + actionBar.setDisplayShowHomeEnabled(true) + actionBar.title = getPageTitle(initialPosition) + } + toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } + toolbar.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.action_open_in_external_app -> openInExternalApp() + R.id.action_download -> requestDownloadMedia() + R.id.action_open_status -> onOpenStatus() + R.id.action_share_media -> shareMedia() + R.id.action_copy_media_link -> copyLink() + } + true + } + + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE + window.statusBarColor = Color.BLACK + window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { + override fun onTransitionEnd(transition: Transition) { + adapter.onTransitionEnd(viewPager.currentItem) + window.sharedElementEnterTransition.removeListener(this) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + if (attachments != null) { + menuInflater.inflate(R.menu.view_media_toolbar, menu) + return true + } + return false + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + menu?.findItem(R.id.action_share_media)?.isEnabled = !isCreating + + if(attachments != null) { + val isStatus = attachments!!.any { it.statusId != null && it.statusUrl != null } + menu?.findItem(R.id.action_open_status)?.isVisible = isStatus + } + return true + } + + override fun onBringUp() { + supportStartPostponedEnterTransition() + } + + override fun onDismiss() { + supportFinishAfterTransition() + } + + override fun onPhotoTap() { + isToolbarVisible = !isToolbarVisible + for (listener in toolbarVisibilityListeners) { + listener(isToolbarVisible) + } + + val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE + 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 + } + + toolbar.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + toolbar.visibility = visibility + animation.removeListener(this) + } + }) + .start() + } + + private fun getPageTitle(position: Int): CharSequence { + if(attachments == null) { + return "" + } + return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size) + } + + private fun downloadMedia() { + val url = attachments!![viewPager.currentItem].attachment.url + val filename = Uri.parse(url).lastPathSegment + Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show() + + val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(Uri.parse(url)) + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, + getString(R.string.app_name) + "/" + filename) + downloadManager.enqueue(request) + } + + private fun requestDownloadMedia() { + requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults -> + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + downloadMedia() + } else { + showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } + } + } + } + + private fun onOpenStatus() { + val attach = attachments!![viewPager.currentItem] + startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) + } + + private fun copyLink() { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url)) + } + + private fun shareMedia() { + val directory = applicationContext.getExternalFilesDir("Husky") + if (directory == null || !(directory.exists())) { + Log.e(TAG, "Error obtaining directory to save temporary media.") + return + } + + val attachment = attachments!![viewPager.currentItem].attachment + when (attachment.type) { + Attachment.Type.IMAGE -> shareImage(directory, attachment.url) + Attachment.Type.AUDIO, + Attachment.Type.VIDEO, + Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url) + else -> Log.e(TAG, "Unknown media format for sharing.") + } + } + + private fun shareFile(file: File, mimeType: String?) { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) + sendIntent.type = mimeType + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) + } + + private fun openInExternalApp() { + val url = attachments!![viewPager.currentItem].attachment.url + val intent = Intent(Intent.ACTION_VIEW) + val extension = MimeTypeMap.getFileExtensionFromUrl(url) + if(extension != null) { + intent.setDataAndType(Uri.parse(url), MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)) + } else { + intent.data = Uri.parse(url) + } + + startActivity(intent) + } + + + private var isCreating: Boolean = false + + private fun shareImage(directory: File, url: String) { + isCreating = true + progressBarShare.visibility = View.VISIBLE + invalidateOptionsMenu() + val file = File(directory, getTemporaryMediaFilename("png")) + val futureTask: FutureTarget = + Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit() + Single.fromCallable { + val bitmap = futureTask.get() + try { + val stream = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + stream.close() + return@fromCallable true + } catch (fnfe: FileNotFoundException) { + Log.e(TAG, "Error writing temporary media.") + } catch (ioe: IOException) { + Log.e(TAG, "Error writing temporary media.") + } + return@fromCallable false + + } + + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose { + futureTask.cancel(true) + } + .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { result -> + Log.d(TAG, "Download image result: $result") + isCreating = false + invalidateOptionsMenu() + progressBarShare.visibility = View.GONE + if (result) + shareFile(file, "image/png") + }, + { error -> + isCreating = false + invalidateOptionsMenu() + progressBarShare.visibility = View.GONE + Log.e(TAG, "Failed to download image", error) + } + ) + + } + + private fun shareMediaFile(directory: File, url: String) { + val uri = Uri.parse(url) + val mimeTypeMap = MimeTypeMap.getSingleton() + val extension = MimeTypeMap.getFileExtensionFromUrl(url) + val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) + val filename = getTemporaryMediaFilename(extension) + val file = File(directory, filename) + + val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(uri) + request.setDestinationUri(Uri.fromFile(file)) + request.setVisibleInDownloadsUi(false) + downloadManager.enqueue(request) + + shareFile(file, mimeType) + } +} + +abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) { + abstract fun onTransitionEnd(position: Int) +} + +interface NoopTransitionListener : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) { + } + + override fun onTransitionResume(transition: Transition) { + } + + override fun onTransitionPause(transition: Transition) { + } + + override fun onTransitionCancel(transition: Transition) { + } + + override fun onTransitionStart(transition: Transition) { + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java new file mode 100644 index 0000000..a49dcc8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -0,0 +1,91 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; + +import com.keylesspalace.tusky.fragment.TimelineFragment; + +import java.util.Collections; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.HasAndroidInjector; + +public class ViewTagActivity extends BottomSheetActivity implements HasAndroidInjector { + + private static final String HASHTAG = "hashtag"; + + @Inject + public DispatchingAndroidInjector dispatchingAndroidInjector; + + public static Intent getIntent(Context context, String tag){ + Intent intent = new Intent(context,ViewTagActivity.class); + intent.putExtra(HASHTAG,tag); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_tag); + + String hashtag = getIntent().getStringExtra(HASHTAG); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + + if (bar != null) { + bar.setTitle(String.format(getString(R.string.title_tag), hashtag)); + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); + } + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment fragment = TimelineFragment.newHashtagInstance(Collections.singletonList(hashtag)); + fragmentTransaction.replace(R.id.fragment_container, fragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public AndroidInjector androidInjector() { + return dispatchingAndroidInjector; + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java new file mode 100644 index 0000000..2267dc5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -0,0 +1,130 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentTransaction; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; + +import com.keylesspalace.tusky.fragment.ViewThreadFragment; +import com.keylesspalace.tusky.util.LinkHelper; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.HasAndroidInjector; + +public class ViewThreadActivity extends BottomSheetActivity implements HasAndroidInjector { + + public static final int REVEAL_BUTTON_HIDDEN = 1; + public static final int REVEAL_BUTTON_REVEAL = 2; + public static final int REVEAL_BUTTON_HIDE = 3; + + public static Intent startIntent(Context context, String id, String url) { + Intent intent = new Intent(context, ViewThreadActivity.class); + intent.putExtra(ID_EXTRA, id); + intent.putExtra(URL_EXTRA, url); + return intent; + } + + private static final String ID_EXTRA = "id"; + private static final String URL_EXTRA = "url"; + private static final String FRAGMENT_TAG = "ViewThreadFragment_"; + + private int revealButtonState = REVEAL_BUTTON_HIDDEN; + + @Inject + public DispatchingAndroidInjector dispatchingAndroidInjector; + + private ViewThreadFragment fragment; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_thread); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.title_view_thread); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + } + + String id = getIntent().getStringExtra(ID_EXTRA); + + fragment = (ViewThreadFragment)getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG + id); + if(fragment == null) { + fragment = ViewThreadFragment.newInstance(id); + } + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id); + fragmentTransaction.commit(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.view_thread_toolbar, menu); + MenuItem menuItem = menu.findItem(R.id.action_reveal); + menuItem.setVisible(revealButtonState != REVEAL_BUTTON_HIDDEN); + menuItem.setIcon(revealButtonState == REVEAL_BUTTON_REVEAL ? + R.drawable.ic_eye_24dp : R.drawable.ic_hide_media_24dp); + return super.onCreateOptionsMenu(menu); + } + + public void setRevealButtonState(int state) { + switch (state) { + case REVEAL_BUTTON_HIDDEN: + case REVEAL_BUTTON_REVEAL: + case REVEAL_BUTTON_HIDE: + this.revealButtonState = state; + invalidateOptionsMenu(); + break; + default: + throw new IllegalArgumentException("Invalid reveal button state: " + state); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + case R.id.action_reveal: { + fragment.onRevealPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public AndroidInjector androidInjector() { + return dispatchingAndroidInjector; + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java new file mode 100644 index 0000000..5c52e39 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -0,0 +1,112 @@ +/* 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.adapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.util.ListUtils; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AccountAdapter extends RecyclerView.Adapter { + static final int VIEW_TYPE_ACCOUNT = 0; + static final int VIEW_TYPE_FOOTER = 1; + + List accountList; + AccountActionListener accountActionListener; + private boolean bottomLoading; + + AccountAdapter(AccountActionListener accountActionListener) { + this.accountList = new ArrayList<>(); + this.accountActionListener = accountActionListener; + bottomLoading = false; + } + + @Override + public int getItemCount() { + return accountList.size() + (bottomLoading ? 1 : 0); + } + + @Override + public int getItemViewType(int position) { + if (position == accountList.size() && bottomLoading) { + return VIEW_TYPE_FOOTER; + } else { + return VIEW_TYPE_ACCOUNT; + } + } + + public void update(@NonNull List newAccounts) { + accountList = ListUtils.removeDuplicates(newAccounts); + notifyDataSetChanged(); + } + + public void addItems(@NonNull List newAccounts) { + int end = accountList.size(); + Account last = accountList.get(end - 1); + if (last != null && !findAccount(newAccounts, last.getId())) { + accountList.addAll(newAccounts); + notifyItemRangeInserted(end, newAccounts.size()); + } + } + + public void setBottomLoading(boolean loading) { + boolean wasLoading = bottomLoading; + if(wasLoading == loading) { + return; + } + bottomLoading = loading; + if(loading) { + notifyItemInserted(accountList.size()); + } else { + notifyItemRemoved(accountList.size()); + } + } + + private static boolean findAccount(@NonNull List accounts, String id) { + for (Account account : accounts) { + if (account.getId().equals(id)) { + return true; + } + } + return false; + } + + @Nullable + public Account removeItem(int position) { + if (position < 0 || position >= accountList.size()) { + return null; + } + Account account = accountList.remove(position); + notifyItemRemoved(position); + return account; + } + + public void addItem(@NonNull Account account, int position) { + if (position < 0 || position > accountList.size()) { + return; + } + accountList.add(position, account); + notifyItemInserted(position); + } + + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt new file mode 100644 index 0000000..e80129c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -0,0 +1,78 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +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.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) : 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 onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val proofOrField = fields[position] + + if(proofOrField.isLeft()) { + val identityProof = proofOrField.asLeft() + + viewHolder.nameTextView.text = identityProof.provider + viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) + + viewHolder.valueTextView.movementMethod = LinkMovementMethod.getInstance() + + viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + } else { + val field = proofOrField.asRight() + val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView) + viewHolder.nameTextView.text = emojifiedName + + val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView) + LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) + + if(field.verifiedAt != null) { + viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + } else { + viewHolder.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 new file mode 100644 index 0000000..768c288 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -0,0 +1,98 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +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 com.keylesspalace.tusky.entity.StringField +import kotlinx.android.synthetic.main.item_edit_field.view.* + +class AccountFieldEditAdapter : RecyclerView.Adapter() { + + private val fieldData = mutableListOf() + + fun setFields(fields: List) { + fieldData.clear() + + fields.forEach { field -> + fieldData.add(MutableStringPair(field.name, field.value)) + } + if(fieldData.isEmpty()) { + fieldData.add(MutableStringPair("", "")) + } + + notifyDataSetChanged() + } + + fun getFieldData(): List { + return fieldData.map { + StringField(it.first, it.second) + } + } + + fun addField() { + fieldData.add(MutableStringPair("", "")) + notifyItemInserted(fieldData.size - 1) + } + + override fun getItemCount(): Int = fieldData.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_edit_field, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + viewHolder.nameTextView.setText(fieldData[position].first) + viewHolder.valueTextView.setText(fieldData[position].second) + + viewHolder.nameTextView.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(newText: Editable) { + fieldData[viewHolder.adapterPosition].first = newText.toString() + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) + + viewHolder.valueTextView.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(newText: Editable) { + fieldData[viewHolder.adapterPosition].second = newText.toString() + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) + + } + + class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) { + val nameTextView: EditText = rootView.accountFieldName + val valueTextView: EditText = rootView.accountFieldValue + } + + class MutableStringPair (var first: String, var second: String) + + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt new file mode 100644 index 0000000..dae0db4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -0,0 +1,57 @@ +/* Copyright 2019 Levi Bard + * + * 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.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.util.* +import kotlinx.android.synthetic.main.item_autocomplete_account.view.* + +class AccountSelectionAdapter(context: Context) : ArrayAdapter(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) + } + view!! + + val account = getItem(position) + if (account != null) { + val username = view.username + val displayName = view.display_name + val avatar = view.avatar + username.text = account.fullName + displayName.text = account.displayName.emojify(account.emojis, displayName) + + val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) + val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context) + .getBoolean("animateGifAvatars", false) + + loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) + + } + + return view + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java new file mode 100644 index 0000000..3ac5968 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -0,0 +1,64 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.SharedPreferences; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.interfaces.LinkListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; + +public class AccountViewHolder extends RecyclerView.ViewHolder { + private TextView username; + private TextView displayName; + private ImageView avatar; + private ImageView avatarInset; + private String accountId; + private boolean showBotOverlay; + private boolean animateAvatar; + + public AccountViewHolder(View itemView) { + super(itemView); + username = itemView.findViewById(R.id.account_username); + displayName = itemView.findViewById(R.id.account_display_name); + avatar = itemView.findViewById(R.id.account_avatar); + avatarInset = itemView.findViewById(R.id.account_avatar_inset); + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); + showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); + animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false); + } + + public void setupWithAccount(Account account) { + accountId = account.getId(); + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.getUsername()); + username.setText(formattedUsername); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, true); + displayName.setText(emojifiedName); + int avatarRadius = avatar.getContext().getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); + if (showBotOverlay && account.getBot()) { + avatarInset.setVisibility(View.VISIBLE); + avatarInset.setImageResource(R.drawable.ic_bot_24dp); + avatarInset.setBackgroundColor(0x50ffffff); + } else { + avatarInset.setVisibility(View.GONE); + } + } + + void setupActionListener(final AccountActionListener listener) { + itemView.setOnClickListener(v -> listener.onViewAccount(accountId)); + } + + public void setupLinkListener(final LinkListener listener) { + itemView.setOnClickListener(v -> listener.onViewAccount(accountId)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt new file mode 100644 index 0000000..6024199 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt @@ -0,0 +1,92 @@ +/* Copyright 2019 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.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.util.onTextChanged +import com.keylesspalace.tusky.util.visible + +class AddPollOptionsAdapter( + private var options: MutableList, + private val maxOptionLength: Int, + private val onOptionRemoved: (Boolean) -> Unit, + private val onOptionChanged: (Boolean) -> Unit +): RecyclerView.Adapter() { + + val pollOptions: List + get() = options.toList() + + fun addChoice() { + options.add("") + 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)) + + holder.editText.onTextChanged { s, _, _, _ -> + val pos = holder.adapterPosition + if(pos != RecyclerView.NO_POSITION) { + options[pos] = s.toString() + onOptionChanged(validateInput()) + } + } + + return holder + } + + override fun getItemCount() = options.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.editText.setText(options[position]) + + holder.textInputLayout.hint = holder.textInputLayout.context.getString(R.string.poll_new_choice_hint, position + 1) + + holder.deleteButton.visible(position > 1, View.INVISIBLE) + + holder.deleteButton.setOnClickListener { + holder.editText.clearFocus() + options.removeAt(holder.adapterPosition) + notifyItemRemoved(holder.adapterPosition) + onOptionRemoved(validateInput()) + } + } + + private fun validateInput(): Boolean { + if (options.contains("") || options.distinct().size != options.size) { + return false + } + + 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/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java new file mode 100644 index 0000000..073d76d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -0,0 +1,109 @@ +/* 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.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; + +public class BlocksAdapter extends AccountAdapter { + + public BlocksAdapter(AccountActionListener accountActionListener) { + super(accountActionListener); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_ACCOUNT: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_blocked_user, parent, false); + return new BlockedUserViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + holder.setupActionListener(accountActionListener); + } + } + + static class BlockedUserViewHolder extends RecyclerView.ViewHolder { + private ImageView avatar; + private TextView username; + private TextView displayName; + private ImageButton unblock; + private String id; + private boolean animateAvatar; + + BlockedUserViewHolder(View itemView) { + super(itemView); + avatar = itemView.findViewById(R.id.blocked_user_avatar); + username = itemView.findViewById(R.id.blocked_user_username); + displayName = itemView.findViewById(R.id.blocked_user_display_name); + unblock = itemView.findViewById(R.id.blocked_user_unblock); + animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) + .getBoolean("animateGifAvatars", false); + + } + + void setupWithAccount(Account account) { + id = account.getId(); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + displayName.setText(emojifiedName); + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.getUsername()); + username.setText(formattedUsername); + int avatarRadius = avatar.getContext().getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); + } + + void setupActionListener(final AccountActionListener listener) { + unblock.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBlock(false, id, position); + } + }); + itemView.setOnClickListener(v -> listener.onViewAccount(id)); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt new file mode 100644 index 0000000..cb4f650 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt @@ -0,0 +1,229 @@ +package com.keylesspalace.tusky.adapter + +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.text.TextUtils +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.TimestampUtils +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.view.MediaPreviewImageView +import com.keylesspalace.tusky.viewdata.ChatMessageViewData +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.roundToInt + +class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) { + object Key { + const val KEY_CREATED = "created" + } + + private val content: TextView = view.findViewById(R.id.content) + private val timestamp: TextView = view.findViewById(R.id.datetime) + private val attachmentView: MediaPreviewImageView = view.findViewById(R.id.attachment) + private val mediaOverlay: ImageView = view.findViewById(R.id.mediaOverlay) + private val attachmentLayout: FrameLayout = view.findViewById(R.id.attachmentLayout) + + private val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) + + private val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(itemView.context, R.attr.colorBackgroundAccent)) + + fun setupWithChatMessage(msg: ChatMessageViewData.Concrete, chatActionListener: ChatActionListener, payload: Any?) { + if(payload == null) { + if(msg.content != null) { + val text = msg.content.emojify(msg.emojis, content) + LinkHelper.setClickableText(content, text, null, chatActionListener) + } + + setAttachment(msg.attachment, chatActionListener) + setCreatedAt(msg.createdAt) + } else { + if(payload is List<*>) { + for (item in payload) { + if (ChatsViewHolder.Key.KEY_CREATED == item) { + setCreatedAt(msg.createdAt) + } + } + } + } + } + + private fun loadImage(imageView: MediaPreviewImageView, + previewUrl: String?, + meta: Attachment.MetaData?) { + if (TextUtils.isEmpty(previewUrl)) { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(mediaPreviewUnloaded) + .centerInside() + .into(imageView) + } else { + val focus = meta?.focus + if (focus != null) { // If there is a focal point for this attachment: + imageView.setFocalPoint(focus) + Glide.with(imageView) + .load(previewUrl) + .placeholder(mediaPreviewUnloaded) + .centerInside() + .addListener(imageView) + .into(imageView) + } else { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(previewUrl) + .placeholder(mediaPreviewUnloaded) + .centerInside() + .into(imageView) + } + } + } + + private fun formatDuration(durationInSeconds: Double): String? { + val seconds = durationInSeconds.roundToInt().toInt() % 60 + val minutes = durationInSeconds.toInt() % 3600 / 60 + val hours = durationInSeconds.toInt() / 3600 + return String.format("%d:%02d:%02d", hours, minutes, seconds) + } + + private fun getAttachmentDescription(context: Context, attachment: Attachment): CharSequence { + var duration = "" + if (attachment.meta?.duration != null && attachment.meta.duration > 0) { + duration = formatDuration(attachment.meta.duration.toDouble()) + " " + } + return if (TextUtils.isEmpty(attachment.description)) { + duration + context.getString(R.string.description_status_media_no_description_placeholder) + } else { + duration + attachment.description + } + } + + + private fun setAttachmentClickListener(view: View, listener: ChatActionListener, attachment: Attachment, animateTransition: Boolean) { + view.setOnClickListener { v: View -> + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onViewMedia(position, if (animateTransition) v else null) + } + } + view.setOnLongClickListener { v: View -> + val description = getAttachmentDescription(v.context, attachment) + Toast.makeText(v.context, description, Toast.LENGTH_LONG).show() + true + } + } + + + private fun setAttachment(attachment: Attachment?, listener: ChatActionListener) { + if(attachment == null) { + attachmentLayout.visibility = View.GONE + } else { + attachmentLayout.visibility = View.VISIBLE + + val previewUrl = attachment.previewUrl + val description = attachment.description + + if(TextUtils.isEmpty(description)) { + attachmentView.contentDescription = description + } else { + attachmentView.contentDescription = attachmentView.context + .getString(R.string.action_view_media) + } + + loadImage(attachmentView, previewUrl, attachment.meta) + + when(attachment.type) { + Attachment.Type.VIDEO, Attachment.Type.GIFV -> { + mediaOverlay.visibility = View.VISIBLE + } + else -> { + mediaOverlay.visibility = View.GONE + } + } + + setAttachmentClickListener(attachmentView, listener, attachment, true) + } + } + + private fun setCreatedAt(createdAt: Date) { + timestamp.text = sdf.format(createdAt) + } +} + +class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource, + private val chatActionListener: ChatActionListener, + private val localUserId: String) +: RecyclerView.Adapter() { + + private val VIEW_TYPE_OUR_MESSAGE = 0 + private val VIEW_TYPE_THEIR_MESSAGE = 1 + private val VIEW_TYPE_PLACEHOLDER = 2 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when(viewType) { + VIEW_TYPE_OUR_MESSAGE -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_our_message, parent, false) + return ChatMessagesViewHolder(view) + } + VIEW_TYPE_THEIR_MESSAGE -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_their_message, parent, false) + return ChatMessagesViewHolder(view) + } + else -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status_placeholder, parent, false) + return PlaceholderViewHolder(view) + } + } + } + + override fun getItemCount(): Int { + return dataSource.itemCount + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(holder, position, null) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList) { + bindViewHolder(holder, position, payload) + } + + private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList?) { + val chat: ChatMessageViewData = dataSource.getItemAt(position) + if(holder is PlaceholderViewHolder) { + holder.setup(chatActionListener, (chat as ChatMessageViewData.Placeholder).isLoading) + } else if(holder is ChatMessagesViewHolder) { + holder.setupWithChatMessage(chat as ChatMessageViewData.Concrete, chatActionListener, + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) + } + } + + override fun getItemViewType(position: Int): Int { + if(dataSource.getItemAt(position) is ChatMessageViewData.Concrete) { + val msg = dataSource.getItemAt(position) as ChatMessageViewData.Concrete + + if(msg.accountId == localUserId) { + return VIEW_TYPE_OUR_MESSAGE + } + return VIEW_TYPE_THEIR_MESSAGE + } + return VIEW_TYPE_PLACEHOLDER + } + + override fun getItemId(position: Int): Long { + return dataSource.getItemAt(position).getViewDataId().toLong() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt new file mode 100644 index 0000000..65a19ab --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ChatsAdapter.kt @@ -0,0 +1,209 @@ +package com.keylesspalace.tusky.adapter + +import android.graphics.Typeface +import android.opengl.Visibility +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.text.toSpanned +import androidx.recyclerview.widget.RecyclerView +import at.connyduck.sparkbutton.helpers.Utils +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.TimestampUtils +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.viewdata.ChatViewData +import java.text.SimpleDateFormat +import java.util.* + +class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) { + object Key { + const val KEY_CREATED = "created" + } + + private val avatar: ImageView = view.findViewById(R.id.status_avatar) + private val avatarInset: ImageView = view.findViewById(R.id.status_avatar_inset) + private val displayName: TextView = view.findViewById(R.id.status_display_name) + private val userName: TextView = view.findViewById(R.id.status_username) + private val timestamp: TextView = view.findViewById(R.id.status_timestamp_info) + private val content: TextView = view.findViewById(R.id.status_content) + private val unread: TextView = view.findViewById(R.id.chat_unread) + + private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) + + fun setupWithChat(chat: ChatViewData.Concrete, + listener: ChatActionListener, + statusDisplayOptions: StatusDisplayOptions, + localUserId: String, + payload: Any?) { + if (payload == null) { + displayName.text = chat.account.displayName?.emojify(chat.account.emojis, displayName, true) + ?: "" + userName.text = userName.context.getString(R.string.status_username_format, chat.account.username) + setUpdatedAt(chat.updatedAt, statusDisplayOptions) + setAvatar(chat.account.avatar, chat.account.bot, statusDisplayOptions) + if (chat.unread <= 0) { + unread.visibility = View.GONE + } else if (chat.unread > 99) { + unread.text = ":)" + } else { + unread.text = chat.unread.toString() + } + avatar.setOnClickListener { listener.onViewAccount(chat.account.id) } + val onLongClickListener = View.OnLongClickListener { + listener.onMore(chat.id, it) + true + } + val onClickListener = View.OnClickListener { + val pos = adapterPosition + if (pos != RecyclerView.NO_POSITION) + listener.openChat(pos) + } + + content.setOnLongClickListener(onLongClickListener) + itemView.setOnLongClickListener(onLongClickListener) + content.setOnClickListener(onClickListener) + itemView.setOnClickListener(onClickListener) + + if(chat.lastMessage != null) { + var text = if (chat.lastMessage.content != null) { + content.setTypeface(null, Typeface.NORMAL) + + chat.lastMessage.content.emojify(chat.lastMessage.emojis, content, true) + } else if (chat.lastMessage.attachment != null) { + content.setTypeface(null, Typeface.ITALIC) + + content.resources.getString(chat.lastMessage.attachment.describeAttachmentType()) + } else if (chat.lastMessage.card != null) { + content.setTypeface(null, Typeface.ITALIC) + + content.resources.getString(R.string.link) + } else "" + + content.text = if(chat.lastMessage.accountId == localUserId) { + SpannableStringBuilder.valueOf(content.resources.getText(R.string.chat_our_last_message)) + .append(": ").append(text) + } else text + + } else { + content.text = "" + } + } else { + if(payload is List<*>) { + for (item in payload as List<*>) { + if (Key.KEY_CREATED == item) { + setUpdatedAt(chat.updatedAt, statusDisplayOptions) + } + } + } + } + } + + private fun setAvatar(url: String, + isBot: Boolean, + statusDisplayOptions: StatusDisplayOptions) { + avatar.setPaddingRelative(0, 0, 0, 0) + if (statusDisplayOptions.showBotOverlay && isBot) { + avatarInset.visibility = View.VISIBLE + avatarInset.setBackgroundColor(0x50ffffff) + Glide.with(avatarInset) + .load(R.drawable.ic_bot_24dp) + .into(avatarInset) + } else { + avatarInset.visibility = View.GONE + } + val avatarRadius = itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp); + loadAvatar(url, avatar, avatarRadius, + statusDisplayOptions.animateAvatars) + } + + + private fun getAbsoluteTime(createdAt: Date?): String? { + if (createdAt == null) { + return "??:??:??" + } + return if (DateUtils.isToday(createdAt.time)) { + shortSdf.format(createdAt) + } else { + longSdf.format(createdAt) + } + } + + private fun setUpdatedAt(updatedAt: Date, statusDisplayOptions: StatusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime) { + timestamp.text = getAbsoluteTime(updatedAt) + } else { + val then = updatedAt.time + val now = System.currentTimeMillis() + val readout = TimestampUtils.getRelativeTimeSpanString(timestamp.context, then, now) + timestamp.text = readout + } + } + +} + +class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource, + val statusDisplayOptions: StatusDisplayOptions, + private val chatActionListener: ChatActionListener, + val localUserId: String) : RecyclerView.Adapter() { + + private val VIEW_TYPE_CHAT = 0 + private val VIEW_TYPE_PLACEHOLDER = 1 + + override fun getItemCount(): Int { + return dataSource.itemCount + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(holder, position, null) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList) { + bindViewHolder(holder, position, payload) + } + + private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList?) { + val chat: ChatViewData = dataSource.getItemAt(position) + if(holder is PlaceholderViewHolder) { + holder.setup(chatActionListener, (chat as ChatViewData.Placeholder).isLoading) + } else if(holder is ChatsViewHolder) { + holder.setupWithChat(chat as ChatViewData.Concrete, chatActionListener, + statusDisplayOptions, localUserId, + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if(viewType == VIEW_TYPE_CHAT ) { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_chat, parent, false) + return ChatsViewHolder(view) + } + // else VIEW_TYPE_PLACEHOLDER + + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status_placeholder, parent, false) + return PlaceholderViewHolder(view) + } + + override fun getItemViewType(position: Int): Int { + if(dataSource.getItemAt(position) is ChatViewData.Concrete) + return VIEW_TYPE_CHAT + + return VIEW_TYPE_PLACEHOLDER + } + + override fun getItemId(position: Int): Long { + return dataSource.getItemAt(position).getViewDataId().toLong() + } +} \ 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 new file mode 100644 index 0000000..70a6163 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -0,0 +1,64 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Emoji +import java.util.* + +class EmojiAdapter(emojiList: List, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter() { + private val emojiList : List + + init { + this.emojiList = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + } + + 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) { + val emoji = emojiList[position] + + Glide.with(viewHolder.emojiImageView) + .load(emoji.url) + .into(viewHolder.emojiImageView) + + viewHolder.emojiImageView.setOnClickListener { + onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) + } + + viewHolder.emojiImageView.contentDescription = emoji.shortcode + } + + class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView) + +} + +interface OnEmojiSelectedListener { + fun onEmojiSelected(shortcode: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java new file mode 100644 index 0000000..c8491ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java @@ -0,0 +1,72 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.emoji.widget.EmojiAppCompatButton; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.DateFormat; +import java.util.List; +import java.util.Date; + + +public class EmojiReactionsAdapter extends RecyclerView.Adapter { + private final List reactions; + private final StatusActionListener listener; + private final String statusId; + + EmojiReactionsAdapter(final List reactions, final StatusActionListener listener, final String statusId) { + this.reactions = reactions; + this.listener = listener; + this.statusId = statusId; + } + + @Override + public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_reaction, parent, false); + return new SingleViewHolder(view); + } + + @Override + public void onBindViewHolder(SingleViewHolder holder, int position) { + EmojiReaction reaction = reactions.get(position); + String str = reaction.getName() + " " + reaction.getCount(); + + // no custom emoji yet! + EmojiAppCompatButton btn = (EmojiAppCompatButton)holder.itemView; + + btn.setText(str); + btn.setActivated(reaction.getMe()); + btn.setOnClickListener(v -> { + listener.onEmojiReactMenu(v, reaction, statusId); + }); + } + + // total number of rows + @Override + public int getItemCount() { + return reactions.size(); + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java new file mode 100644 index 0000000..8215874 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -0,0 +1,61 @@ +/* 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.adapter; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.AccountActionListener; + +/** Both for follows and following lists. */ +public class FollowAdapter extends AccountAdapter { + + public FollowAdapter(AccountActionListener accountActionListener) { + super(accountActionListener); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_ACCOUNT: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_account, parent, false); + return new AccountViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + AccountViewHolder holder = (AccountViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + holder.setupActionListener(accountActionListener); + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt new file mode 100644 index 0000000..e39e500 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -0,0 +1,58 @@ +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.entity.Account +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible +import 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 + private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context) + .getBoolean("animateGifAvatars", false) + + fun setupWithAccount(account: Account) { + id = account.id + val wrappedName = account.name.unicodeWrap() + val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, true) + itemView.displayNameTextView.text = emojifiedName + if (showHeader) { + val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) + itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { + setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + }.emojify(account.emojis, itemView) + } + itemView.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) + } + + fun setupActionListener(listener: AccountActionListener) { + itemView.acceptButton.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onRespondToFollowRequest(true, id, position) + } + } + itemView.rejectButton.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onRespondToFollowRequest(false, id, position) + } + } + itemView.setOnClickListener { listener.onViewAccount(id) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java new file mode 100644 index 0000000..dab3d4f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -0,0 +1,60 @@ +/* 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.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.AccountActionListener; + +public class FollowRequestsAdapter extends AccountAdapter { + + public FollowRequestsAdapter(AccountActionListener accountActionListener) { + super(accountActionListener); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + 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); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + holder.setupActionListener(accountActionListener); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt new file mode 100644 index 0000000..c70076c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..f9b19c6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt @@ -0,0 +1,41 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import com.keylesspalace.tusky.R +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) { + 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 + } + + return view + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt new file mode 100644 index 0000000..ebff5c5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt @@ -0,0 +1,21 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import android.view.View + +class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java new file mode 100644 index 0000000..66138b2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java @@ -0,0 +1,170 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.TimestampUtils; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class MutedStatusViewHolder extends RecyclerView.ViewHolder { + public static class Key { + public static final String KEY_CREATED = "created"; + } + + private TextView displayName; + private TextView username; + private ImageButton unmuteButton; + public TextView timestampInfo; + + private SimpleDateFormat shortSdf; + private SimpleDateFormat longSdf; + + protected MutedStatusViewHolder(View itemView) { + super(itemView); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); + unmuteButton = itemView.findViewById(R.id.status_toggle_mute); + + this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); + } + + protected void setDisplayName(String name, List customEmojis) { + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName, true); + displayName.setText(emojifiedName); + } + + protected void setUsername(String name) { + Context context = username.getContext(); + String usernameText = context.getString(R.string.status_username_format, name); + username.setText(usernameText); + } + + protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + timestampInfo.setText(getAbsoluteTime(createdAt)); + } else { + if (createdAt == null) { + timestampInfo.setText("?m"); + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + timestampInfo.setText(readout); + } + } + } + + private String getAbsoluteTime(Date createdAt) { + if (createdAt == null) { + return "??:??:??"; + } + if (DateUtils.isToday(createdAt.getTime())) { + return shortSdf.format(createdAt); + } else { + return longSdf.format(createdAt); + } + } + + private CharSequence getCreatedAtDescription(Date createdAt, + StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + return getAbsoluteTime(createdAt); + } else { + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + + if (createdAt == null) { + return "? minutes"; + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + return DateUtils.getRelativeTimeSpanString(then, now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + } + } + + private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, + StatusDisplayOptions statusDisplayOptions) { + Context context = itemView.getContext(); + + String description = context.getString(R.string.description_muted_status, + status.getUserFullName(), + getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + status.getNickname() + ); + itemView.setContentDescription(description); + } + + + protected void setupButtons(final StatusActionListener listener, final String accountId) { + + unmuteButton.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMute(position, false); + } + }); + + itemView.setOnClickListener( v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }); + } + + public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions) { + this.setupWithStatus(status, listener, statusDisplayOptions, null); + } + + protected void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setUsername(status.getNickname()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + + setupButtons(listener, status.getSenderId()); + setDescriptionForStatus(status, statusDisplayOptions); + + // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 + // RecyclerView tries to set AccessibilityDelegateCompat to null + // but ViewCompat code replaces is with the default one. RecyclerView never + // fetches another one from its delegate because it checks that it's set so we remove it + // and let RecyclerView ask for a new delegate. + itemView.setAccessibilityDelegate(null); + } else { + if (payloads instanceof List) + for (Object item : (List) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + } + } + + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java new file mode 100644 index 0000000..c4224c9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -0,0 +1,135 @@ +package com.keylesspalace.tusky.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +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; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; + +import java.util.HashMap; + +public class MutesAdapter extends AccountAdapter { + private HashMap mutingNotificationsMap; + + public MutesAdapter(AccountActionListener accountActionListener) { + super(accountActionListener); + mutingNotificationsMap = new HashMap(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_ACCOUNT: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_muted_user, parent, false); + return new MutesAdapter.MutedUserViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; + Account account = accountList.get(position); + holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId())); + holder.setupActionListener(accountActionListener); + } + } + + public void updateMutingNotifications(String id, boolean mutingNotifications, int position) { + mutingNotificationsMap.put(id, mutingNotifications); + notifyItemChanged(position); + } + + public void updateMutingNotificationsMap(HashMap newMutingNotificationsMap) { + mutingNotificationsMap.putAll(newMutingNotificationsMap); + notifyDataSetChanged(); + } + + static class MutedUserViewHolder extends RecyclerView.ViewHolder { + private ImageView avatar; + private TextView username; + private TextView displayName; + private ImageButton unmute; + private ImageButton muteNotifications; + private String id; + private boolean animateAvatar; + private boolean notifications; + + MutedUserViewHolder(View itemView) { + super(itemView); + avatar = itemView.findViewById(R.id.muted_user_avatar); + username = itemView.findViewById(R.id.muted_user_username); + displayName = itemView.findViewById(R.id.muted_user_display_name); + unmute = itemView.findViewById(R.id.muted_user_unmute); + muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); + animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) + .getBoolean("animateGifAvatars", false); + } + + void setupWithAccount(Account account, Boolean mutingNotifications) { + id = account.getId(); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + displayName.setText(emojifiedName); + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.getUsername()); + username.setText(formattedUsername); + int avatarRadius = avatar.getContext().getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); + + String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername); + unmute.setContentDescription(unmuteString); + ViewCompat.setTooltipText(unmute, unmuteString); + + if (mutingNotifications == null) { + muteNotifications.setEnabled(false); + notifications = true; + } else { + muteNotifications.setEnabled(true); + notifications = mutingNotifications; + } + + if (notifications) { + muteNotifications.setImageResource(R.drawable.ic_notifications_24dp); + String unmuteNotificationsString = muteNotifications.getContext() + .getString(R.string.action_unmute_notifications_desc, formattedUsername); + muteNotifications.setContentDescription(unmuteNotificationsString); + ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString); + } else { + muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp); + String muteNotificationsString = muteNotifications.getContext() + .getString(R.string.action_mute_notifications_desc, formattedUsername); + muteNotifications.setContentDescription(muteNotificationsString); + ViewCompat.setTooltipText(muteNotifications, muteNotificationsString); + } + } + + void setupActionListener(final AccountActionListener listener) { + unmute.setOnClickListener(v -> listener.onMute(false, id, getAdapterPosition(), false)); + muteNotifications.setOnClickListener( + v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); + itemView.setOnClickListener(v -> listener.onViewAccount(id)); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt new file mode 100644 index 0000000..66065c7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt @@ -0,0 +1,45 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +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, + private val retryCallback: () -> Unit) +: RecyclerView.ViewHolder(itemView) { + + 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 { + retryCallback() + } + if(fullScreen) { + itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + } else { + itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java new file mode 100644 index 0000000..51fb5e1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -0,0 +1,734 @@ +/* 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.adapter; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.InputFilter; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.interfaces.LinkListener; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.SmartLengthInputFilter; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; +import com.keylesspalace.tusky.util.ThemeUtils; +import com.keylesspalace.tusky.util.TimestampUtils; +import com.keylesspalace.tusky.viewdata.NotificationViewData; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import at.connyduck.sparkbutton.helpers.Utils; + +public class NotificationsAdapter extends RecyclerView.Adapter { + + public interface AdapterDataSource { + int getItemCount(); + + T getItemAt(int pos); + } + + + private static final int VIEW_TYPE_STATUS = 0; + private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1; + private static final int VIEW_TYPE_FOLLOW = 2; + private static final int VIEW_TYPE_PLACEHOLDER = 3; + private static final int VIEW_TYPE_MUTED_STATUS = 4; + private static final int VIEW_TYPE_FOLLOW_REQUEST = 5; + private static final int VIEW_TYPE_MOVE = 6; + private static final int VIEW_TYPE_UNKNOWN = 7; + + private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; + + private String accountId; + private StatusDisplayOptions statusDisplayOptions; + private StatusActionListener statusListener; + private NotificationActionListener notificationActionListener; + private AccountActionListener accountActionListener; + private AdapterDataSource dataSource; + + public NotificationsAdapter(String accountId, + AdapterDataSource dataSource, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener statusListener, + NotificationActionListener notificationActionListener, + AccountActionListener accountActionListener) { + + this.accountId = accountId; + this.dataSource = dataSource; + this.statusDisplayOptions = statusDisplayOptions; + this.statusListener = statusListener; + this.notificationActionListener = notificationActionListener; + this.accountActionListener = accountActionListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (viewType) { + case VIEW_TYPE_STATUS: { + View view = inflater + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_MUTED_STATUS: { + View view = inflater + .inflate(R.layout.item_status_muted, parent, false); + return new MutedStatusViewHolder(view); + } + case VIEW_TYPE_STATUS_NOTIFICATION: { + View view = inflater + .inflate(R.layout.item_status_notification, parent, false); + return new StatusNotificationViewHolder(view, statusDisplayOptions); + } + case VIEW_TYPE_MOVE: + case VIEW_TYPE_FOLLOW: { + View view = inflater + .inflate(R.layout.item_follow, parent, false); + 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); + } + case VIEW_TYPE_PLACEHOLDER: { + View view = inflater + .inflate(R.layout.item_status_placeholder, parent, false); + return new PlaceholderViewHolder(view); + } + default: + case VIEW_TYPE_UNKNOWN: { + View view = new View(parent.getContext()); + view.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + Utils.dpToPx(parent.getContext(), 24) + ) + ); + return new RecyclerView.ViewHolder(view) { + }; + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + bindViewHolder(viewHolder, position, null); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { + bindViewHolder(viewHolder, position, payloads); + } + + private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { + Object payloadForHolder = payloads != null && !payloads.isEmpty() ? payloads.get(0) : null; + if (position < this.dataSource.getItemCount()) { + NotificationViewData notification = dataSource.getItemAt(position); + if (notification instanceof NotificationViewData.Placeholder) { + if (payloadForHolder == null) { + NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification); + PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; + holder.setup(statusListener, placeholder.isLoading()); + } + return; + } + NotificationViewData.Concrete concreteNotificaton = + (NotificationViewData.Concrete) notification; + switch (viewHolder.getItemViewType()) { + case VIEW_TYPE_STATUS: { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); + holder.setupWithStatus(status, + statusListener, statusDisplayOptions, payloadForHolder); + if (concreteNotificaton.getType() == Notification.Type.POLL) { + holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId())); + } else { + holder.hideStatusInfo(); + } + break; + } + case VIEW_TYPE_MUTED_STATUS: { + MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); + holder.setupWithStatus(status, + statusListener, statusDisplayOptions, payloadForHolder); + break; + } + case VIEW_TYPE_STATUS_NOTIFICATION: { + StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; + StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData(); + if (payloadForHolder == null) { + if (statusViewData == null) { + holder.showNotificationContent(false); + } else { + holder.showNotificationContent(true); + + holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis()); + holder.setUsername(statusViewData.getNickname()); + holder.setCreatedAt(statusViewData.getCreatedAt()); + + if(concreteNotificaton.getType() == Notification.Type.STATUS) { + holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot()); + } else { + holder.setAvatars(statusViewData.getAvatar(), + concreteNotificaton.getAccount().getAvatar()); + } + } + + holder.setMessage(concreteNotificaton, statusListener); + holder.setupButtons(notificationActionListener, + concreteNotificaton.getAccount().getId(), + concreteNotificaton.getId()); + } else { + if (payloadForHolder instanceof List) + for (Object item : (List) payloadForHolder) { + if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) { + holder.setCreatedAt(statusViewData.getCreatedAt()); + } + } + } + break; + } + case VIEW_TYPE_FOLLOW: { + if (payloadForHolder == null) { + FollowViewHolder holder = (FollowViewHolder) viewHolder; + holder.setMessage(concreteNotificaton.getAccount(), null); + holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); + } + break; + } + case VIEW_TYPE_MOVE: { + if (payloadForHolder == null) { + FollowViewHolder holder = (FollowViewHolder) viewHolder; + holder.setMessage(concreteNotificaton.getTarget(), concreteNotificaton.getAccount()); + holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); + } + break; + } + case VIEW_TYPE_FOLLOW_REQUEST: { + if (payloadForHolder == null) { + FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; + holder.setupWithAccount(concreteNotificaton.getAccount()); + holder.setupActionListener(accountActionListener); + } + } + default: + } + } + } + + @Override + public int getItemCount() { + return dataSource.getItemCount(); + } + + public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) { + this.statusDisplayOptions = statusDisplayOptions.copy( + statusDisplayOptions.animateAvatars(), + mediaPreviewEnabled, + statusDisplayOptions.useAbsoluteTime(), + statusDisplayOptions.showBotOverlay(), + statusDisplayOptions.useBlurhash(), + CardViewMode.NONE, + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.renderStatusAsMention(), + statusDisplayOptions.hideStats() + ); + } + + public boolean isMediaPreviewEnabled() { + return this.statusDisplayOptions.mediaPreviewEnabled(); + } + + @Override + public int getItemViewType(int position) { + NotificationViewData notification = dataSource.getItemAt(position); + if (notification instanceof NotificationViewData.Concrete) { + NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification); + switch (concrete.getType()) { + case MENTION: + case POLL: { + if (concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) + return VIEW_TYPE_MUTED_STATUS; + return VIEW_TYPE_STATUS; + } + case STATUS: + if (statusDisplayOptions.renderStatusAsMention()) { + if (concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) + return VIEW_TYPE_MUTED_STATUS; + return VIEW_TYPE_STATUS; + } + /* fallthrough */ + case FAVOURITE: + case REBLOG: + case EMOJI_REACTION: { + return VIEW_TYPE_STATUS_NOTIFICATION; + } + case FOLLOW: { + return VIEW_TYPE_FOLLOW; + } + case FOLLOW_REQUEST: { + return VIEW_TYPE_FOLLOW_REQUEST; + } + case MOVE: { + return VIEW_TYPE_MOVE; + } + default: { + return VIEW_TYPE_UNKNOWN; + } + } + } else if (notification instanceof NotificationViewData.Placeholder) { + return VIEW_TYPE_PLACEHOLDER; + } else { + throw new AssertionError("Unknown notification type"); + } + + + } + + public interface NotificationActionListener { + void onViewAccount(String id); + + void onViewStatusForNotificationId(String notificationId); + + void onExpandedChange(boolean expanded, int position); + + /** + * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long + * status content is interacted with. + * + * @param isCollapsed Whether the status content is shown in a collapsed state or fully. + * @param position The position of the status in the list. + */ + void onNotificationContentCollapsedChange(boolean isCollapsed, int position); + + void onViewReplyTo(int position); + } + + private static class FollowViewHolder extends RecyclerView.ViewHolder { + private TextView message; + private TextView usernameView; + private TextView displayNameView; + private ImageView avatar; + private StatusDisplayOptions statusDisplayOptions; + + FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { + super(itemView); + message = itemView.findViewById(R.id.notification_text); + usernameView = itemView.findViewById(R.id.notification_username); + displayNameView = itemView.findViewById(R.id.notification_display_name); + avatar = itemView.findViewById(R.id.notification_avatar); + this.statusDisplayOptions = statusDisplayOptions; + } + + void setMessage(Account account, @Nullable Account from) { + Context context = message.getContext(); + + String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); + Drawable drawable; + CharSequence emojifiedMessage; + + if(from != null) { + String format = context.getString(R.string.notification_move_format); + String wrappedFromName = StringUtils.unicodeWrap(from.getName()); + String wholeMessage = String.format(format, wrappedFromName); + emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, from.getEmojis(), message, true); + + drawable = ContextCompat.getDrawable(context, R.drawable.ic_reply_24dp); + } else { + String format = context.getString(R.string.notification_follow_format); + String wholeMessage = String.format(format, wrappedDisplayName); + emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message, true); + + drawable = ContextCompat.getDrawable(context, R.drawable.ic_person_add_24dp); + } + + message.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null); + + message.setText(emojifiedMessage); + + String username = context.getString(R.string.status_username_format, account.getUsername()); + usernameView.setText(username); + + CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView, true); + + displayNameView.setText(emojifiedDisplayName); + + int avatarRadius = avatar.getContext().getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_42dp); + + ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, + statusDisplayOptions.animateAvatars()); + + } + + void setupButtons(final NotificationActionListener listener, final String accountId) { + itemView.setOnClickListener(v -> listener.onViewAccount(accountId)); + } + } + + + private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener { + private final TextView message; + private final View statusNameBar; + private final TextView displayName; + private final TextView username; + private final TextView timestampInfo; + private final TextView statusContent; + private final ImageView statusAvatar; + private final ImageView notificationAvatar; + private final TextView replyInfo; + private final TextView contentWarningDescriptionTextView; + private final Button contentWarningButton; + private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder + private StatusDisplayOptions statusDisplayOptions; + + private String accountId; + private String notificationId; + private NotificationActionListener notificationActionListener; + private StatusViewData.Concrete statusViewData; + private SimpleDateFormat shortSdf; + private SimpleDateFormat longSdf; + + private int avatarRadius48dp; + private int avatarRadius36dp; + private int avatarRadius24dp; + + StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { + super(itemView); + message = itemView.findViewById(R.id.notification_top_text); + statusNameBar = itemView.findViewById(R.id.status_name_bar); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); + statusContent = itemView.findViewById(R.id.notification_content); + statusAvatar = itemView.findViewById(R.id.notification_status_avatar); + notificationAvatar = itemView.findViewById(R.id.notification_notification_avatar); + replyInfo = itemView.findViewById(R.id.notification_reply_info); + contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description); + contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button); + contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content); + this.statusDisplayOptions = statusDisplayOptions; + + int darkerFilter = Color.rgb(123, 123, 123); + statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); + notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); + + itemView.setOnClickListener(this); + message.setOnClickListener(this); + statusContent.setOnClickListener(this); + shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); + + this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); + } + + private void showNotificationContent(boolean show) { + statusNameBar.setVisibility(show ? View.VISIBLE : View.GONE); + contentWarningDescriptionTextView.setVisibility(show ? View.VISIBLE : View.GONE); + contentWarningButton.setVisibility(show ? View.VISIBLE : View.GONE); + statusContent.setVisibility(show ? View.VISIBLE : View.GONE); + statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE); + replyInfo.setVisibility(show ? View.VISIBLE : View.GONE); + } + + private void setDisplayName(String name, List emojis) { + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, true); + displayName.setText(emojifiedName); + } + + private void setUsername(String name) { + Context context = username.getContext(); + String format = context.getString(R.string.status_username_format); + String usernameText = String.format(format, name); + username.setText(usernameText); + } + + protected void setCreatedAt(@Nullable Date createdAt) { + if (statusDisplayOptions.useAbsoluteTime()) { + String time; + if (createdAt != null) { + if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) { + time = longSdf.format(createdAt); + } else { + time = shortSdf.format(createdAt); + } + } else { + time = "??:??:??"; + } + timestampInfo.setText(time); + } else { + // This is the visible timestampInfo. + String readout; + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + CharSequence readoutAloud; + if (createdAt != null) { + long then = createdAt.getTime(); + long now = new Date().getTime(); + readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, + android.text.format.DateUtils.SECOND_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); + } else { + // unknown minutes~ + readout = "?m"; + readoutAloud = "? minutes"; + } + timestampInfo.setText(readout); + timestampInfo.setContentDescription(readoutAloud); + } + } + + void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { + this.statusViewData = notificationViewData.getStatusViewData(); + + String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName()); + Notification.Type type = notificationViewData.getType(); + + Context context = message.getContext(); + String wholeMessage; + Drawable icon; + switch (type) { + default: + case FAVOURITE: { + icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.tusky_orange), PorterDuff.Mode.SRC_ATOP); + } + + String format = context.getString(R.string.notification_favourite_format); + wholeMessage = String.format(format, displayName); + break; + } + case REBLOG: { + icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); + } + + String format = context.getString(R.string.notification_reblog_format); + wholeMessage = String.format(format, displayName); + break; + } + case STATUS: { + icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); + } + + String format = context.getString(R.string.notification_subscription_format); + wholeMessage = String.format(format, displayName); + break; + } + case EMOJI_REACTION: { + icon = ContextCompat.getDrawable(context, R.drawable.ic_emoji_24dp); + if(icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.tusky_green), PorterDuff.Mode.SRC_ATOP); + } + + String format = context.getString(R.string.notification_emoji_format); + String emojiCode = notificationViewData.getEmoji(); + wholeMessage = String.format(format, displayName, emojiCode); + break; + } + } + message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); + final SpannableString str = new SpannableString(wholeMessage); + str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + CharSequence emojifiedText = CustomEmojiHelper.emojify(str, notificationViewData.getAccount().getEmojis(), message, true); + message.setText(emojifiedText); + + if (statusViewData != null) { + boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); + contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); + contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); + if (statusViewData.isExpanded()) { + contentWarningButton.setText(R.string.status_content_warning_show_less); + } else { + contentWarningButton.setText(R.string.status_content_warning_show_more); + } + + contentWarningButton.setOnClickListener(view -> { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getAdapterPosition()); + } + statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE); + }); + + setupContentAndSpoiler(listener); + setupReplyInfo(); + } + + } + + void setupButtons(final NotificationActionListener listener, final String accountId, + final String notificationId) { + this.notificationActionListener = listener; + this.accountId = accountId; + this.notificationId = notificationId; + } + + void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) { + statusAvatar.setPaddingRelative(0, 0, 0, 0); + + ImageLoadingHelper.loadAvatar(statusAvatarUrl, + statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars()); + + if (statusDisplayOptions.showBotOverlay() && isBot) { + notificationAvatar.setVisibility(View.VISIBLE); + notificationAvatar.setBackgroundColor(0x50ffffff); + Glide.with(notificationAvatar) + .load(R.drawable.ic_bot_24dp) + .into(notificationAvatar); + + } else { + notificationAvatar.setVisibility(View.GONE); + } + } + + void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) { + int padding = Utils.dpToPx(statusAvatar.getContext(), 12); + statusAvatar.setPaddingRelative(0, 0, padding, padding); + + ImageLoadingHelper.loadAvatar(statusAvatarUrl, + statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars()); + + notificationAvatar.setVisibility(View.VISIBLE); + ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, + avatarRadius24dp, statusDisplayOptions.animateAvatars()); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.notification_container: + case R.id.notification_content: + if (notificationActionListener != null) + notificationActionListener.onViewStatusForNotificationId(notificationId); + break; + case R.id.notification_top_text: + if (notificationActionListener != null) + notificationActionListener.onViewAccount(accountId); + break; + } + } + + private void setupContentAndSpoiler(final LinkListener listener) { + boolean shouldShowContentIfSpoiler = statusViewData.isExpanded(); + boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); + if (!shouldShowContentIfSpoiler && hasSpoiler) { + statusContent.setVisibility(View.GONE); + } else { + statusContent.setVisibility(View.VISIBLE); + } + + Spanned content = statusViewData.getContent(); + List emojis = statusViewData.getStatusEmojis(); + + if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { + contentCollapseButton.setOnClickListener(view -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION && notificationActionListener != null) { + notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position); + } + }); + + contentCollapseButton.setVisibility(View.VISIBLE); + if (statusViewData.isCollapsed()) { + contentCollapseButton.setText(R.string.status_content_warning_show_more); + statusContent.setFilters(COLLAPSE_INPUT_FILTER); + } else { + contentCollapseButton.setText(R.string.status_content_warning_show_less); + statusContent.setFilters(NO_INPUT_FILTER); + } + } else { + contentCollapseButton.setVisibility(View.GONE); + statusContent.setFilters(NO_INPUT_FILTER); + } + + CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); + LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); + + CharSequence emojifiedContentWarning = + CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); + contentWarningDescriptionTextView.setText(emojifiedContentWarning); + } + + private void setupReplyInfo() { + if (statusViewData.getInReplyToId() != null) { + Context context = replyInfo.getContext(); + String replyToAccount = statusViewData.getInReplyToAccountAcct(); + replyInfo.setText(context.getString(R.string.status_replied_to_format, replyToAccount)); + if (!statusViewData.getParentVisible()) { + replyInfo.setPaintFlags(replyInfo.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + replyInfo.setOnClickListener(null); + } else { + replyInfo.setPaintFlags(replyInfo.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + replyInfo.setOnClickListener(v -> notificationActionListener.onViewReplyTo(getAdapterPosition())); + } + replyInfo.setVisibility(View.VISIBLE); + } else { + replyInfo.setVisibility(View.GONE); + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java new file mode 100644 index 0000000..403fc7d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java @@ -0,0 +1,60 @@ +/* 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.adapter; + +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.ChatActionListener; +import com.keylesspalace.tusky.interfaces.StatusActionListener; + +public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { + + private Button loadMoreButton; + private ProgressBar progressBar; + + PlaceholderViewHolder(View itemView) { + super(itemView); + loadMoreButton = itemView.findViewById(R.id.button_load_more); + progressBar = itemView.findViewById(R.id.progressBar); + } + + private void setup(boolean progress) { + loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE); + progressBar.setVisibility(progress ? View.VISIBLE : View.GONE); + + loadMoreButton.setEnabled(true); + } + + public void setup(final StatusActionListener listener, boolean progress) { + setup(progress); + loadMoreButton.setOnClickListener(v -> { + loadMoreButton.setEnabled(false); + listener.onLoadMore(getAdapterPosition()); + }); + } + + public void setup(final ChatActionListener listener, boolean progress) { + setup(progress); + loadMoreButton.setOnClickListener( v -> { + loadMoreButton.setEnabled(false); + listener.onLoadMore(getAdapterPosition()); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt new file mode 100644 index 0000000..6255a88 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -0,0 +1,130 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +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.entity.Emoji +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewdata.PollOptionViewData +import com.keylesspalace.tusky.viewdata.buildDescription +import com.keylesspalace.tusky.viewdata.calculatePercent + +class PollAdapter: RecyclerView.Adapter() { + + private var pollOptions: List = emptyList() + private var voteCount: Int = 0 + private var votersCount: Int? = null + private var mode = RESULT + private var emojis: List = emptyList() + private var resultClickListener: View.OnClickListener? = null + + fun setup( + options: List, + voteCount: Int, + votersCount: Int?, + emojis: List, + mode: Int, + resultClickListener: View.OnClickListener?) { + this.pollOptions = options + this.voteCount = voteCount + this.votersCount = votersCount + this.emojis = emojis + this.mode = mode + this.resultClickListener = resultClickListener + notifyDataSetChanged() + } + + fun getSelected() : List { + return pollOptions.filter { it.selected } + .map { pollOptions.indexOf(it) } + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollViewHolder { + return PollViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll, parent, false)) + } + + override fun getItemCount(): Int { + return pollOptions.size + } + + override fun onBindViewHolder(holder: PollViewHolder, position: Int) { + + val option = pollOptions[position] + + holder.resultTextView.visible(mode == RESULT) + holder.radioButton.visible(mode == SINGLE) + holder.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) + holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) + + val level = percent * 100 + + holder.resultTextView.background.level = level + holder.resultTextView.setOnClickListener(resultClickListener) + } + SINGLE -> { + val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) + holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) + holder.radioButton.isChecked = option.selected + holder.radioButton.setOnClickListener { + pollOptions.forEachIndexed { index, pollOption -> + pollOption.selected = index == holder.adapterPosition + notifyItemChanged(index) + } + } + } + MULTIPLE -> { + val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox) + holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) + holder.checkBox.isChecked = option.selected + holder.checkBox.setOnCheckedChangeListener { _, isChecked -> + pollOptions[holder.adapterPosition].selected = isChecked + } + } + } + + } + + companion object { + const val RESULT = 0 + const val SINGLE = 1 + 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 new file mode 100644 index 0000000..328e962 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt @@ -0,0 +1,67 @@ +/* Copyright 2019 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.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.widget.TextViewCompat +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R + +class PreviewPollOptionsAdapter: RecyclerView.Adapter() { + + private var options: List = emptyList() + private var multiple: Boolean = false + private var clickListener: View.OnClickListener? = null + + fun update(newOptions: List, multiple: Boolean) { + this.options = newOptions + this.multiple = multiple + notifyDataSetChanged() + } + + fun setOnClickListener(l: View.OnClickListener?) { + clickListener = l + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder { + return PreviewViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll_preview_option, parent, false)) + } + + override fun getItemCount() = options.size + + override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) { + val textView = holder.itemView as TextView + + val iconId = if (multiple) { + R.drawable.ic_check_box_outline_blank_18dp + } else { + R.drawable.ic_radio_button_unchecked_18dp + } + + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, iconId, 0, 0, 0) + + textView.text = options[position] + + textView.setOnClickListener(clickListener) + } + +} + + +class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java new file mode 100644 index 0000000..6d4889c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java @@ -0,0 +1,122 @@ +/* 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.adapter; + +import android.content.Context; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.db.TootEntity; + +import java.util.ArrayList; +import java.util.List; + +public class SavedTootAdapter extends RecyclerView.Adapter { + private List list; + private SavedTootAction handler; + + public SavedTootAdapter(Context context) { + super(); + list = new ArrayList<>(); + handler = (SavedTootAction) context; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_saved_toot, parent, false); + return new TootViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + TootViewHolder holder = (TootViewHolder) viewHolder; + holder.bind(getItem(position)); + } + + @Override + public int getItemCount() { + return list.size(); + } + + public void setItems(List newToot) { + list = new ArrayList<>(); + list.addAll(newToot); + } + + public void addItems(List newToot) { + int end = list.size(); + list.addAll(newToot); + notifyItemRangeInserted(end, newToot.size()); + } + + @Nullable + public TootEntity removeItem(int position) { + if (position < 0 || position >= list.size()) { + return null; + } + TootEntity toot = list.remove(position); + notifyItemRemoved(position); + return toot; + } + + private TootEntity getItem(int position) { + if (position >= 0 && position < list.size()) { + return list.get(position); + } + return null; + } + + // handler saved toot + public interface SavedTootAction { + void delete(int position, TootEntity item); + + void click(int position, TootEntity item); + } + + private class TootViewHolder extends RecyclerView.ViewHolder { + View view; + TextView content; + ImageButton suppr; + + TootViewHolder(View view) { + super(view); + this.view = view; + this.content = view.findViewById(R.id.content); + this.suppr = view.findViewById(R.id.suppr); + } + + void bind(final TootEntity item) { + suppr.setEnabled(true); + + if (item != null) { + content.setText(item.getText()); + + suppr.setOnClickListener(v -> { + v.setEnabled(false); + handler.delete(getAdapterPosition(), item); + }); + view.setOnClickListener(v -> handler.click(getAdapterPosition(), item)); + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java new file mode 100644 index 0000000..26dfbb6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.adapter; + +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; + +// empty class to be able to instantiate ViewHolder which is abstract for dumbass reason +public class SingleViewHolder extends RecyclerView.ViewHolder { + public SingleViewHolder(View view) { + super(view); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java new file mode 100644 index 0000000..16460ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -0,0 +1,1166 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.MotionEvent; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import android.util.Log; +import android.graphics.Paint; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.text.HtmlCompat; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; +import com.google.android.material.button.MaterialButton; +import com.google.android.flexbox.FlexboxLayoutManager; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Attachment.Focus; +import com.keylesspalace.tusky.entity.Attachment.MetaData; +import com.keylesspalace.tusky.entity.Card; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.*; +import com.keylesspalace.tusky.view.MediaPreviewImageView; +import com.keylesspalace.tusky.view.EmojiKeyboard; +import com.keylesspalace.tusky.viewdata.PollOptionViewData; +import com.keylesspalace.tusky.viewdata.PollViewData; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import at.connyduck.sparkbutton.SparkButton; +import at.connyduck.sparkbutton.helpers.Utils; +import kotlin.collections.CollectionsKt; + +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + +public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { + public static class Key { + public static final String KEY_CREATED = "created"; + } + + private TextView displayName; + private TextView username; + private TextView replyInfo; + private ImageButton replyButton; + private SparkButton reblogButton; + private SparkButton favouriteButton; + private SparkButton bookmarkButton; + private ImageButton reactButton; + private ImageButton moreButton; + protected MediaPreviewImageView[] mediaPreviews; + private ImageView[] mediaOverlays; + private TextView sensitiveMediaWarning; + private View sensitiveMediaShow; + protected TextView[] mediaLabels; + protected CharSequence[] mediaDescriptions; + private MaterialButton contentWarningButton; + private ImageView avatarInset; + + public ImageView avatar; + public TextView timestampInfo; + public TextView content; + public TextView contentWarningDescription; + + private RecyclerView pollOptions; + private TextView pollDescription; + private Button pollButton; + + private LinearLayout cardView; + private LinearLayout cardInfo; + private ImageView cardImage; + private TextView cardTitle; + private TextView cardDescription; + private TextView cardUrl; + private PollAdapter pollAdapter; + + private SimpleDateFormat shortSdf; + private SimpleDateFormat longSdf; + + private final NumberFormat numberFormat = NumberFormat.getNumberInstance(); + + protected int avatarRadius48dp; + private int avatarRadius36dp; + private int avatarRadius24dp; + + private final Drawable mediaPreviewUnloaded; + + private RecyclerView emojiReactionsView; + + protected StatusBaseViewHolder(View itemView) { + super(itemView); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); + content = itemView.findViewById(R.id.status_content); + avatar = itemView.findViewById(R.id.status_avatar); + replyInfo = itemView.findViewById(R.id.reply_info); + replyButton = itemView.findViewById(R.id.status_reply); + reblogButton = itemView.findViewById(R.id.status_inset); + favouriteButton = itemView.findViewById(R.id.status_favourite); + bookmarkButton = itemView.findViewById(R.id.status_bookmark); + moreButton = itemView.findViewById(R.id.status_more); + reactButton = itemView.findViewById(R.id.status_emoji_react); + emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions); + + /* Disabled, because it doesn't handle parent resizes. It must be fixed and can be enabled again */ + /* float INCREASE_HORIZONTAL_HIT_AREA = 20.0f; + + ViewExtensionsKt.increaseHitArea(replyButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA); + if(reblogButton != null) + ViewExtensionsKt.increaseHitArea(reblogButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA); + ViewExtensionsKt.increaseHitArea(favouriteButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA); + ViewExtensionsKt.increaseHitArea(bookmarkButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA); + ViewExtensionsKt.increaseHitArea(moreButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA); */ + + itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true); + + mediaPreviews = new MediaPreviewImageView[]{ + itemView.findViewById(R.id.status_media_preview_0), + itemView.findViewById(R.id.status_media_preview_1), + itemView.findViewById(R.id.status_media_preview_2), + itemView.findViewById(R.id.status_media_preview_3) + }; + mediaOverlays = new ImageView[]{ + itemView.findViewById(R.id.status_media_overlay_0), + itemView.findViewById(R.id.status_media_overlay_1), + itemView.findViewById(R.id.status_media_overlay_2), + itemView.findViewById(R.id.status_media_overlay_3) + }; + sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); + sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button); + mediaLabels = new TextView[]{ + itemView.findViewById(R.id.status_media_label_0), + itemView.findViewById(R.id.status_media_label_1), + itemView.findViewById(R.id.status_media_label_2), + itemView.findViewById(R.id.status_media_label_3) + }; + mediaDescriptions = new CharSequence[mediaLabels.length]; + contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description); + contentWarningButton = itemView.findViewById(R.id.status_content_warning_button); + avatarInset = itemView.findViewById(R.id.status_avatar_inset); + + pollOptions = itemView.findViewById(R.id.status_poll_options); + pollDescription = itemView.findViewById(R.id.status_poll_description); + pollButton = itemView.findViewById(R.id.status_poll_button); + + cardView = itemView.findViewById(R.id.status_card_view); + cardInfo = itemView.findViewById(R.id.card_info); + cardImage = itemView.findViewById(R.id.card_image); + cardTitle = itemView.findViewById(R.id.card_title); + cardDescription = itemView.findViewById(R.id.card_description); + cardUrl = itemView.findViewById(R.id.card_link); + + pollAdapter = new PollAdapter(); + pollOptions.setAdapter(pollAdapter); + pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); + ((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false); + + this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); + + this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); + + mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); + } + + protected abstract int getMediaPreviewHeight(Context context); + + protected void setDisplayName(String name, List customEmojis) { + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName, true); + displayName.setText(emojifiedName); + } + + protected void setUsername(String name) { + Context context = username.getContext(); + String usernameText = context.getString(R.string.status_username_format, name); + username.setText(usernameText); + } + + public void toggleContentWarning() { + contentWarningButton.performClick(); + } + + protected void setSpoilerAndContent(boolean expanded, + @NonNull Spanned content, + @Nullable String spoilerText, + @Nullable Status.Mention[] mentions, + @NonNull List emojis, + @Nullable PollViewData poll, + @NonNull StatusDisplayOptions statusDisplayOptions, + final StatusActionListener listener) { + boolean sensitive = !TextUtils.isEmpty(spoilerText); + if (sensitive) { + CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); + contentWarningDescription.setText(emojiSpoiler); + contentWarningDescription.setVisibility(View.VISIBLE); + contentWarningButton.setVisibility(View.VISIBLE); + setContentWarningButtonText(expanded); + contentWarningButton.setOnClickListener(view -> { + contentWarningDescription.invalidate(); + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(!expanded, getAdapterPosition()); + } + setContentWarningButtonText(!expanded); + + this.setTextVisible(sensitive, !expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); + }); + this.setTextVisible(sensitive, expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); + } else { + contentWarningDescription.setVisibility(View.GONE); + contentWarningButton.setVisibility(View.GONE); + this.setTextVisible(sensitive, true, content, mentions, emojis, poll, statusDisplayOptions, listener); + } + } + + private void setContentWarningButtonText(boolean expanded) { + if (expanded) { + contentWarningButton.setText(R.string.status_content_warning_show_less); + } else { + contentWarningButton.setText(R.string.status_content_warning_show_more); + } + } + + private void setTextVisible(boolean sensitive, + boolean expanded, + Spanned content, + Status.Mention[] mentions, + List emojis, + @Nullable PollViewData poll, + StatusDisplayOptions statusDisplayOptions, + final StatusActionListener listener) { + if (expanded) { + CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); + LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); + for (int i = 0; i < mediaLabels.length; ++i) { + updateMediaLabel(i, sensitive, expanded); + } + if (poll != null) { + setupPoll(poll, emojis, statusDisplayOptions, listener); + } else { + hidePoll(); + } + } else { + hidePoll(); + LinkHelper.setClickableMentions(this.content, mentions, listener); + } + if (TextUtils.isEmpty(this.content.getText())) { + this.content.setVisibility(View.GONE); + } else { + this.content.setVisibility(View.VISIBLE); + } + } + + private void hidePoll() { + pollButton.setVisibility(View.GONE); + pollDescription.setVisibility(View.GONE); + pollOptions.setVisibility(View.GONE); + } + + private void setAvatar(String url, + @Nullable String rebloggedUrl, + boolean isBot, + StatusDisplayOptions statusDisplayOptions) { + + int avatarRadius; + if (TextUtils.isEmpty(rebloggedUrl)) { + avatar.setPaddingRelative(0, 0, 0, 0); + + if (statusDisplayOptions.showBotOverlay() && isBot) { + avatarInset.setVisibility(View.VISIBLE); + avatarInset.setBackgroundColor(0x50ffffff); + Glide.with(avatarInset) + .load(R.drawable.ic_bot_24dp) + .into(avatarInset); + + } else { + avatarInset.setVisibility(View.GONE); + } + + avatarRadius = avatarRadius48dp; + + } else { + int padding = Utils.dpToPx(avatar.getContext(), 12); + avatar.setPaddingRelative(0, 0, padding, padding); + + avatarInset.setVisibility(View.VISIBLE); + avatarInset.setBackground(null); + ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, + statusDisplayOptions.animateAvatars()); + + avatarRadius = avatarRadius36dp; + } + + ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, + statusDisplayOptions.animateAvatars()); + + } + + protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + timestampInfo.setText(getAbsoluteTime(createdAt)); + } else { + if (createdAt == null) { + timestampInfo.setText("?m"); + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + timestampInfo.setText(readout); + } + } + } + + private String getAbsoluteTime(Date createdAt) { + if (createdAt == null) { + return "??:??:??"; + } + if (DateUtils.isToday(createdAt.getTime())) { + return shortSdf.format(createdAt); + } else { + return longSdf.format(createdAt); + } + } + + private CharSequence getCreatedAtDescription(Date createdAt, + StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + return getAbsoluteTime(createdAt); + } else { + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + + if (createdAt == null) { + return "? minutes"; + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + return DateUtils.getRelativeTimeSpanString(then, now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + } + } + + protected void setIsReply(boolean isReply) { + if (isReply) { + replyButton.setImageResource(R.drawable.ic_reply_all_24dp); + } else { + replyButton.setImageResource(R.drawable.ic_reply_24dp); + } + + } + + protected void setReplyInfo(StatusViewData.Concrete status, StatusActionListener listener) { + if (status.getInReplyToId() != null) { + Context context = replyInfo.getContext(); + String replyToAccount = status.getInReplyToAccountAcct(); + replyInfo.setText(context.getString(R.string.status_replied_to_format, replyToAccount)); + if (!status.getParentVisible()) { + replyInfo.setPaintFlags(replyInfo.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + replyInfo.setOnClickListener(null); + } else { + replyInfo.setPaintFlags(replyInfo.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + replyInfo.setOnClickListener(v -> listener.onViewReplyTo(getAdapterPosition())); + } + replyInfo.setVisibility(View.VISIBLE); + } else { + replyInfo.setVisibility(View.GONE); + } + } + + private void setReblogged(boolean reblogged) { + reblogButton.setChecked(reblogged); + } + + // This should only be called after setReblogged, in order to override the tint correctly. + private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) { + reblogButton.setEnabled(enabled && visibility != Status.Visibility.PRIVATE); + + if (enabled) { + int inactiveId; + int activeId; + if (visibility == Status.Visibility.PRIVATE) { + inactiveId = R.drawable.ic_reblog_private_24dp; + activeId = R.drawable.ic_reblog_private_active_24dp; + } else { + inactiveId = R.drawable.ic_reblog_24dp; + activeId = R.drawable.ic_reblog_active_24dp; + } + reblogButton.setInactiveImage(inactiveId); + reblogButton.setActiveImage(activeId); + } else { + int disabledId; + if (visibility == Status.Visibility.DIRECT) { + disabledId = R.drawable.ic_reblog_direct_24dp; + } else { + disabledId = R.drawable.ic_reblog_private_24dp; + } + reblogButton.setInactiveImage(disabledId); + reblogButton.setActiveImage(disabledId); + } + } + + protected void setFavourited(boolean favourited) { + favouriteButton.setChecked(favourited); + } + + protected void setBookmarked(boolean bookmarked) { + bookmarkButton.setChecked(bookmarked); + } + + private BitmapDrawable decodeBlurHash(String blurhash) { + return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash); + } + + private void loadImage(MediaPreviewImageView imageView, + @Nullable String previewUrl, + @Nullable MetaData meta, + @Nullable String blurhash) { + + Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded; + + if (TextUtils.isEmpty(previewUrl)) { + imageView.removeFocalPoint(); + + Glide.with(imageView) + .load(placeholder) + .centerInside() + .into(imageView); + + } else { + Focus focus = meta != null ? meta.getFocus() : null; + + if (focus != null) { // If there is a focal point for this attachment: + imageView.setFocalPoint(focus); + + Glide.with(imageView) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(imageView) + .into(imageView); + } else { + imageView.removeFocalPoint(); + + Glide.with(imageView) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .into(imageView); + } + } + } + + protected void setMediaPreviews(final List attachments, boolean sensitive, + final StatusActionListener listener, boolean showingContent, + boolean useBlurhash) { + Context context = itemView.getContext(); + final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS); + + + final int mediaPreviewHeight = getMediaPreviewHeight(context); + + if (n <= 2) { + mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight * 2; + mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight * 2; + } else { + mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight; + mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight; + mediaPreviews[2].getLayoutParams().height = mediaPreviewHeight; + mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight; + } + + for (int i = 0; i < n; i++) { + Attachment attachment = attachments.get(i); + String previewUrl = attachment.getPreviewUrl(); + String description = attachment.getDescription(); + MediaPreviewImageView imageView = mediaPreviews[i]; + + imageView.setVisibility(View.VISIBLE); + + if (TextUtils.isEmpty(description)) { + imageView.setContentDescription(imageView.getContext() + .getString(R.string.action_view_media)); + } else { + imageView.setContentDescription(description); + } + + loadImage( + imageView, + showingContent ? previewUrl : null, + attachment.getMeta(), + useBlurhash ? attachment.getBlurhash() : null + ); + + final Attachment.Type type = attachment.getType(); + if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) { + mediaOverlays[i].setVisibility(View.VISIBLE); + } else { + mediaOverlays[i].setVisibility(View.GONE); + } + + setAttachmentClickListener(imageView, listener, i, attachment, true); + } + + if (sensitive) { + sensitiveMediaWarning.setText(R.string.status_sensitive_media_title); + } else { + sensitiveMediaWarning.setText(R.string.status_media_hidden_title); + } + + 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()); + } + v.setVisibility(View.GONE); + sensitiveMediaWarning.setVisibility(View.VISIBLE); + }); + sensitiveMediaWarning.setOnClickListener(v -> { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.VISIBLE); + }); + + + // Hide any of the placeholder previews beyond the ones set. + for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { + mediaPreviews[i].setVisibility(View.GONE); + } + } + + @DrawableRes + private static int getLabelIcon(Attachment.Type type) { + switch (type) { + case IMAGE: + return R.drawable.ic_photo_24dp; + case GIFV: + case VIDEO: + return R.drawable.ic_videocam_24dp; + case AUDIO: + return R.drawable.ic_music_box_24dp; + default: + return R.drawable.ic_attach_file_24dp; + } + } + + private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) { + Context context = itemView.getContext(); + CharSequence label = (sensitive && !showingContent) ? + context.getString(R.string.status_sensitive_media_title) : + mediaDescriptions[index]; + mediaLabels[index].setText(label); + } + + protected void setMediaLabel(List attachments, boolean sensitive, + final StatusActionListener listener, boolean showingContent) { + Context context = itemView.getContext(); + for (int i = 0; i < mediaLabels.length; i++) { + TextView mediaLabel = mediaLabels[i]; + if (i < attachments.size()) { + Attachment attachment = attachments.get(i); + mediaLabel.setVisibility(View.VISIBLE); + mediaDescriptions[i] = getAttachmentDescription(context, attachment); + updateMediaLabel(i, sensitive, showingContent); + + // Set the icon next to the label. + int drawableId = getLabelIcon(attachments.get(0).getType()); + mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawableId, 0, 0, 0); + + setAttachmentClickListener(mediaLabel, listener, i, attachment, false); + } else { + mediaLabel.setVisibility(View.GONE); + } + } + } + + private void setAttachmentClickListener(View view, StatusActionListener listener, + int index, Attachment attachment, boolean animateTransition) { + view.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } else { + listener.onViewMedia(position, index, animateTransition ? v : null); + } + } + }); + view.setOnLongClickListener(v -> { + CharSequence description = getAttachmentDescription(view.getContext(), attachment); + Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show(); + return true; + }); + } + + private static CharSequence getAttachmentDescription(Context context, Attachment attachment) { + String duration = ""; + if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) { + duration = formatDuration(attachment.getMeta().getDuration()) + " "; + } + if (TextUtils.isEmpty(attachment.getDescription())) { + return duration + context.getString(R.string.description_status_media_no_description_placeholder); + } else { + return duration + attachment.getDescription(); + } + } + + protected void hideSensitiveMediaWarning() { + sensitiveMediaWarning.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.GONE); + } + + protected void setupButtons(final StatusActionListener listener, + final String accountId, + final String statusContent, + StatusDisplayOptions statusDisplayOptions) { + View.OnClickListener profileButtonClickListener = button -> { + listener.onViewAccount(accountId); + }; + + avatar.setOnClickListener(profileButtonClickListener); + displayName.setOnClickListener(profileButtonClickListener); + + replyButton.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReply(position); + } + }); + if (reblogButton != null) { + reblogButton.setEventListener((button, buttonState) -> { + // return true to play animaion + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (statusDisplayOptions.confirmReblogs()) { + showConfirmReblogDialog(listener, statusContent, buttonState, position); + return false; + } else { + listener.onReblog(!buttonState, position); + return true; + } + } else { + return false; + } + }); + } + + favouriteButton.setEventListener((button, buttonState) -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onFavourite(!buttonState, position); + } + return true; + }); + + bookmarkButton.setEventListener((button, buttonState) -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBookmark(!buttonState, position); + } + return true; + }); + + moreButton.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMore(v, position); + } + }); + + /* Even though the content TextView is a child of the container, it won't respond to clicks + * if it contains URLSpans without also setting its listener. The surrounding spans will + * 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(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; + content.setOnClickListener(viewThreadListener); + itemView.setOnClickListener(viewThreadListener); + } + + private void showConfirmReblogDialog(StatusActionListener listener, + String statusContent, + boolean buttonState, + int position) { + int okButtonTextId = buttonState ? R.string.action_unreblog : R.string.action_reblog; + new AlertDialog.Builder(reblogButton.getContext()) + .setMessage(statusContent) + .setPositiveButton(okButtonTextId, (__, ___) -> { + listener.onReblog(!buttonState, position); + if (!buttonState) { + // Play animation only when it's reblog, not unreblog + reblogButton.playAnimation(); + } + }) + .show(); + } + + private void setEmojiReactions(@Nullable List reactions, final StatusActionListener listener, final String statusId) { + if(reactButton != null) { + reactButton.setOnClickListener(v -> { + EmojiKeyboard.show(reactButton.getContext(), statusId, EmojiKeyboard.UNICODE_MODE, (id, emoji) -> { + listener.onEmojiReact(true, emoji, id); + }); + }); + } + + if(emojiReactionsView != null ) { + if(reactions != null && reactions.size() > 0) { + emojiReactionsView.setVisibility(View.VISIBLE); + FlexboxLayoutManager lm = new FlexboxLayoutManager(emojiReactionsView.getContext()); + emojiReactionsView.setLayoutManager(lm); + emojiReactionsView.setAdapter(new EmojiReactionsAdapter(reactions, listener, statusId)); + emojiReactionsView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if(event.getAction() == MotionEvent.ACTION_POINTER_UP || + event.getAction() == MotionEvent.ACTION_UP) { + int position = getAdapterPosition(); + if(position != RecyclerView.NO_POSITION) + listener.onViewThread(position); + } + return false; + } + }); + } else { + emojiReactionsView.setVisibility(View.GONE); + emojiReactionsView.setLayoutManager(null); + emojiReactionsView.setAdapter(null); + } + } + } + + public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions) { + this.setupWithStatus(status, listener, statusDisplayOptions, null); + } + + protected void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setUsername(status.getNickname()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + setIsReply(status.getInReplyToId() != null); + setReplyInfo(status, listener); + setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions); + setReblogged(status.isReblogged()); + setFavourited(status.isFavourited()); + setBookmarked(status.isBookmarked()); + List attachments = status.getAttachments(); + boolean sensitive = status.isSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); + + if (attachments.size() == 0) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, status.isShowingContent()); + // Hide all unused views. + mediaPreviews[0].setVisibility(View.GONE); + mediaPreviews[1].setVisibility(View.GONE); + mediaPreviews[2].setVisibility(View.GONE); + mediaPreviews[3].setVisibility(View.GONE); + hideSensitiveMediaWarning(); + } + + if (cardView != null) { + setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions); + } + + setupButtons(listener, status.getSenderId(), status.getContent().toString(), + statusDisplayOptions); + setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); + + setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener); + + setDescriptionForStatus(status, statusDisplayOptions); + + setEmojiReactions(status.getEmojiReactions(), listener, status.getId()); + + // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 + // RecyclerView tries to set AccessibilityDelegateCompat to null + // but ViewCompat code replaces is with the default one. RecyclerView never + // fetches another one from its delegate because it checks that it's set so we remove it + // and let RecyclerView ask for a new delegate. + itemView.setAccessibilityDelegate(null); + } else { + if (payloads instanceof List) + for (Object item : (List) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + } + } + + } + } + + protected static boolean hasPreviewableAttachment(List attachments) { + for (Attachment attachment : attachments) { + if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { + return false; + } + } + return true; + } + + private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, + StatusDisplayOptions statusDisplayOptions) { + Context context = itemView.getContext(); + + String description = context.getString(R.string.description_status, + status.getUserFullName(), + getContentWarningDescription(context, status), + (TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""), + getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + getReblogDescription(context, status), + status.getNickname(), + status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "", + status.isFavourited() ? context.getString(R.string.description_status_favourited) : "", + status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "", + getMediaDescription(context, status), + getVisibilityDescription(context, status.getVisibility()), + getFavsText(context, status.getFavouritesCount()), + getReblogsText(context, status.getReblogsCount()), + getPollDescription(status, context, statusDisplayOptions) + ); + itemView.setContentDescription(description); + } + + private static CharSequence getReblogDescription(Context context, + @NonNull StatusViewData.Concrete status) { + String rebloggedUsername = status.getRebloggedByUsername(); + if (rebloggedUsername != null) { + return context + .getString(R.string.status_boosted_format, rebloggedUsername); + } else { + return ""; + } + } + + private static CharSequence getMediaDescription(Context context, + @NonNull StatusViewData.Concrete status) { + if (status.getAttachments().isEmpty()) { + return ""; + } + StringBuilder mediaDescriptions = CollectionsKt.fold( + status.getAttachments(), + new StringBuilder(), + (builder, a) -> { + if (a.getDescription() == null) { + String placeholder = + context.getString(R.string.description_status_media_no_description_placeholder); + return builder.append(placeholder); + } else { + builder.append("; "); + return builder.append(a.getDescription()); + } + }); + return context.getString(R.string.description_status_media, mediaDescriptions); + } + + private static CharSequence getContentWarningDescription(Context context, + @NonNull StatusViewData.Concrete status) { + if (!TextUtils.isEmpty(status.getSpoilerText())) { + return context.getString(R.string.description_status_cw, status.getSpoilerText()); + } else { + return ""; + } + } + + private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) { + + if (visibility == null) { + return ""; + } + + int resource; + switch (visibility) { + case PUBLIC: + resource = R.string.description_visiblity_public; + break; + case UNLISTED: + resource = R.string.description_visiblity_unlisted; + break; + case PRIVATE: + resource = R.string.description_visiblity_private; + break; + case DIRECT: + resource = R.string.description_visiblity_direct; + break; + default: + return ""; + } + return context.getString(resource); + } + + private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status, + Context context, + StatusDisplayOptions statusDisplayOptions) { + PollViewData poll = status.getPoll(); + if (poll == null) { + return ""; + } else { + Object[] args = new CharSequence[5]; + List options = poll.getOptions(); + for (int i = 0; i < args.length; i++) { + if (i < options.size()) { + int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount()); + args[i] = buildDescription(options.get(i).getTitle(), percent, context); + } else { + args[i] = ""; + } + } + args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions, + context); + return context.getString(R.string.description_poll, args); + } + } + + protected CharSequence getFavsText(Context context, int count) { + if (count > 0) { + String countString = numberFormat.format(count); + return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + } else { + return ""; + } + } + + protected CharSequence getReblogsText(Context context, int count) { + if (count > 0) { + String countString = numberFormat.format(count); + return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + } else { + return ""; + } + } + + private void setupPoll(PollViewData poll, List emojis, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { + long timestamp = System.currentTimeMillis(); + + boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime()); + + Context context = pollDescription.getContext(); + + pollOptions.setVisibility(View.VISIBLE); + + if (expired || poll.getVoted()) { + // no voting possible + View.OnClickListener viewThreadListener = v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; + pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); + + pollButton.setVisibility(View.GONE); + } else { + // voting possible + pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); + + pollButton.setVisibility(View.VISIBLE); + + pollButton.setOnClickListener(v -> { + + int position = getAdapterPosition(); + + if (position != RecyclerView.NO_POSITION) { + + List pollResult = pollAdapter.getSelected(); + + if (!pollResult.isEmpty()) { + listener.onVoteInPoll(position, pollResult); + } + } + + }); + } + + pollDescription.setVisibility(View.VISIBLE); + pollDescription.setText(getPollInfoText(timestamp, poll, statusDisplayOptions, context)); + } + + private CharSequence getPollInfoText(long timestamp, PollViewData poll, + StatusDisplayOptions statusDisplayOptions, + Context context) { + + String votesText; + if(poll.getVotersCount() == null) { + String voters = numberFormat.format(poll.getVotesCount()); + votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters); + } else { + String voters = numberFormat.format(poll.getVotersCount()); + votesText = context.getResources().getQuantityString(R.plurals.poll_info_people, poll.getVotersCount(), voters); + } + CharSequence pollDurationInfo; + if (poll.getExpired()) { + pollDurationInfo = context.getString(R.string.poll_info_closed); + } else if (poll.getExpiresAt() == null) { + return votesText; + } else { + if (statusDisplayOptions.useAbsoluteTime()) { + pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt())); + } else { + pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); + } + } + + return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); + } + + protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) { + if (cardViewMode != CardViewMode.NONE && + status.getAttachments().size() == 0 && + status.getCard() != null && + !TextUtils.isEmpty(status.getCard().getUrl()) && + (!status.isCollapsible() || !status.isCollapsed())) { + final Card card = status.getCard(); + cardView.setVisibility(View.VISIBLE); + cardTitle.setText(card.getTitle()); + if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { + cardDescription.setVisibility(View.GONE); + } else { + cardDescription.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(card.getDescription())) { + cardDescription.setText(card.getAuthorName()); + } else { + cardDescription.setText(card.getDescription()); + } + } + + cardUrl.setText(card.getUrl()); + + // Statuses from other activitypub sources can be marked sensitive even if there's no media, + // so let's blur the preview in that case + // If media previews are disabled, show placeholder for cards as well + if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) { + + int topLeftRadius = 0; + int topRightRadius = 0; + int bottomRightRadius = 0; + int bottomLeftRadius = 0; + + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); + + if (card.getWidth() > card.getHeight()) { + cardView.setOrientation(LinearLayout.VERTICAL); + + cardImage.getLayoutParams().height = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_vertical_height); + cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + topLeftRadius = radius; + topRightRadius = radius; + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + topLeftRadius = radius; + bottomLeftRadius = radius; + } + + RequestBuilder builder = Glide.with(cardImage).load(card.getImage()); + if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + builder = builder.placeholder(decodeBlurHash(card.getBlurhash())); + } + builder.transform( + new CenterCrop(), + new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) + ) + .into(cardImage); + } else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); + + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + Glide.with(cardImage).load(decodeBlurHash(card.getBlurhash())) + .transform( + new CenterCrop(), + new GranularRoundedCorners(radius, 0, 0, radius) + ) + .into(cardImage); + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.setImageResource(R.drawable.card_image_placeholder); + } + + cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); + cardView.setClipToOutline(true); + } else { + cardView.setVisibility(View.GONE); + } + } + + private static String formatDuration(double durationInSeconds) { + int seconds = (int) Math.round(durationInSeconds) % 60; + int minutes = (int) durationInSeconds % 3600 / 60; + int hours = (int) durationInSeconds / 3600; + + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java new file mode 100644 index 0000000..77fc90e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -0,0 +1,193 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.emoji.widget.EmojiAppCompatButton; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.flexbox.FlexboxLayoutManager; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.DateFormat; +import java.util.List; +import java.util.Date; + +class StatusDetailedViewHolder extends StatusBaseViewHolder { + private TextView reblogs; + private TextView favourites; + private View infoDivider; + + StatusDetailedViewHolder(View view) { + super(view); + reblogs = view.findViewById(R.id.status_reblogs); + favourites = view.findViewById(R.id.status_favourites); + infoDivider = view.findViewById(R.id.status_info_divider); + } + + @Override + protected int getMediaPreviewHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.status_detail_media_preview_height); + } + + @Override + protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) { + if (createdAt == null) { + timestampInfo.setText(""); + } else { + DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT); + timestampInfo.setText(dateFormat.format(createdAt)); + } + } + + private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) { + + if (reblogCount > 0) { + reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount)); + reblogs.setVisibility(View.VISIBLE); + } else { + reblogs.setVisibility(View.GONE); + } + if (favCount > 0) { + favourites.setText(getFavsText(favourites.getContext(), favCount)); + favourites.setVisibility(View.VISIBLE); + } else { + favourites.setVisibility(View.GONE); + } + + if (reblogs.getVisibility() == View.GONE && favourites.getVisibility() == View.GONE) { + infoDivider.setVisibility(View.GONE); + } else { + infoDivider.setVisibility(View.VISIBLE); + } + + reblogs.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowReblogs(position); + } + }); + favourites.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowFavs(position); + } + }); + } + + private void setApplication(@Nullable Status.Application app) { + if (app != null) { + + timestampInfo.append(" • "); + + if (app.getWebsite() != null) { + CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite()); + timestampInfo.append(text); + timestampInfo.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + timestampInfo.append(app.getName()); + } + } + } + + @Override + protected void setupWithStatus(final StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + super.setupWithStatus(status, listener, statusDisplayOptions, payloads); + setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status + if (payloads == null) { + + if (!statusDisplayOptions.hideStats()) { + setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); + } else { + hideQuantitativeStats(); + } + + setApplication(status.getApplication()); + View.OnLongClickListener longClickListener = view -> { + TextView textView = (TextView) view; + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("toot", textView.getText()); + clipboard.setPrimaryClip(clip); + + Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show(); + + return true; + }; + + content.setOnLongClickListener(longClickListener); + contentWarningDescription.setOnLongClickListener(longClickListener); + setStatusVisibility(status.getVisibility()); + } + } + + private void setStatusVisibility(Status.Visibility visibility) { + + if (visibility == null) { + return; + } + + int visibilityIcon; + switch (visibility) { + case PUBLIC: + visibilityIcon = R.drawable.ic_public_24dp; + break; + case UNLISTED: + visibilityIcon = R.drawable.ic_lock_open_24dp; + break; + case PRIVATE: + visibilityIcon = R.drawable.ic_lock_outline_24dp; + break; + case DIRECT: + visibilityIcon = R.drawable.ic_email_24dp; + break; + default: + return; + } + + final Drawable visibilityDrawable = this.timestampInfo.getContext() + .getDrawable(visibilityIcon); + if (visibilityDrawable == null) { + return; + } + + final int size = (int) this.timestampInfo.getTextSize(); + visibilityDrawable.setBounds( + 0, + 0, + size, + size + ); + visibilityDrawable.setTint(this.timestampInfo.getCurrentTextColor()); + this.timestampInfo.setCompoundDrawables( + visibilityDrawable, + null, + null, + null + ); + } + + private void hideQuantitativeStats() { + reblogs.setVisibility(View.GONE); + favourites.setVisibility(View.GONE); + infoDivider.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java new file mode 100644 index 0000000..4043f90 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -0,0 +1,132 @@ +/* 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.adapter; + +import android.content.Context; +import android.text.InputFilter; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.ImageButton; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.SmartLengthInputFilter; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import at.connyduck.sparkbutton.helpers.Utils; + +public class StatusViewHolder extends StatusBaseViewHolder { + private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; + + private TextView statusInfo; + private Button contentCollapseButton; + private ImageButton toggleVisibility; + + public StatusViewHolder(View itemView) { + super(itemView); + statusInfo = itemView.findViewById(R.id.status_info); + contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); + toggleVisibility = itemView.findViewById(R.id.status_toggle_mute); + } + + @Override + protected int getMediaPreviewHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); + } + + @Override + protected void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + + setupCollapsedState(status, listener); + + String rebloggedByDisplayName = status.getRebloggedByUsername(); + if (rebloggedByDisplayName == null) { + hideStatusInfo(); + } else { + setRebloggedByDisplayName(rebloggedByDisplayName, status); + statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); + } + + if(status.isUserMuted() || status.isThreadMuted()) { + toggleVisibility.setVisibility(View.VISIBLE); + toggleVisibility.setOnClickListener(v -> listener.onMute(getAdapterPosition(), true)); + } else { + toggleVisibility.setVisibility(View.GONE); + } + + } + super.setupWithStatus(status, listener, statusDisplayOptions, payloads); + + } + + private void setRebloggedByDisplayName(final CharSequence name, final StatusViewData.Concrete status) { + Context context = statusInfo.getContext(); + CharSequence wrappedName = StringUtils.unicodeWrap(name); + CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); + CharSequence emojifiedText = CustomEmojiHelper.emojify(boostedText, status.getRebloggedByAccountEmojis(), statusInfo); + statusInfo.setText(emojifiedText); + statusInfo.setVisibility(View.VISIBLE); + } + + // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed + void setPollInfo(final boolean ownPoll) { + statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted); + statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0); + statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10)); + statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.getContext(), 28), 0, 0, 0); + statusInfo.setVisibility(View.VISIBLE); + } + + void hideStatusInfo() { + statusInfo.setVisibility(View.GONE); + } + + private void setupCollapsedState(final StatusViewData.Concrete status, final StatusActionListener listener) { + /* 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(); + if (position != RecyclerView.NO_POSITION) + listener.onContentCollapsedChange(!status.isCollapsed(), position); + }); + + contentCollapseButton.setVisibility(View.VISIBLE); + if (status.isCollapsed()) { + contentCollapseButton.setText(R.string.status_content_warning_show_more); + content.setFilters(COLLAPSE_INPUT_FILTER); + } else { + contentCollapseButton.setText(R.string.status_content_warning_show_less); + content.setFilters(NO_INPUT_FILTER); + } + } else { + contentCollapseButton.setVisibility(View.GONE); + content.setFilters(NO_INPUT_FILTER); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StickerAdapater.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/StickerAdapater.kt new file mode 100644 index 0000000..eec2952 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StickerAdapater.kt @@ -0,0 +1,117 @@ +package com.keylesspalace.tusky.adapter + +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageButton +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.StickerPack +import com.keylesspalace.tusky.view.EmojiKeyboard +import com.keylesspalace.tusky.view.EmojiKeyboard.EmojiKeyboardAdapter +import java.util.* + +class StickerAdapter( + private val stickerPacks: Array, + private val listener: EmojiKeyboard.OnEmojiSelectedListener + ) : RecyclerView.Adapter(), TabConfigurationStrategy, EmojiKeyboardAdapter { + + private val recentsAdapter = StickerPageAdapter(null, listener, emptyList()) + // this value doesn't reflect actual button width but how much we want for button to take space + // this is bad, only villains do that + private val BUTTON_WIDTH_DP = 90.0f + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + if (position == 0) { + tab.setIcon(R.drawable.ic_access_time) + return + } + + val pack = stickerPacks[position - 1] + val imageView = ImageView(tab.view.context) + imageView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT) + Glide.with(imageView) + .asDrawable() + .load(pack.internal_url + pack.tabIcon) + .thumbnail() + .centerCrop() + .into( object: CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) { + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + // tab.icon = resource + imageView.setImageDrawable(resource) + tab.customView = imageView + } + }) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_emoji_keyboard_page, parent, false) + val holder = SingleViewHolder(view) + + val dm = parent.context.resources.displayMetrics + val wdp = dm.widthPixels / dm.density + val rows = (wdp / BUTTON_WIDTH_DP + 0.5).toInt() + + (view as RecyclerView).layoutManager = GridLayoutManager(view.getContext(), rows) + return holder + } + + override fun getItemCount(): Int { + return stickerPacks.size + 1 + } + + override fun onRecentsUpdate(set: MutableSet) { + val list = set.toMutableList() + list.reverse() + recentsAdapter.stickers = list + recentsAdapter.notifyDataSetChanged() + } + + override fun onBindViewHolder(holder: SingleViewHolder, position: Int) { + if( position == 0 ) { + (holder.itemView as RecyclerView).adapter = recentsAdapter + } else { + val pack = stickerPacks[position - 1] + (holder.itemView as RecyclerView).adapter = StickerPageAdapter(pack.internal_url, listener, pack.stickers) + } + } + + private class StickerPageAdapter( + private val url: String?, + var listener: EmojiKeyboard.OnEmojiSelectedListener, + var stickers: List + ) : RecyclerView.Adapter() { + override fun getItemCount(): Int { + return stickers.size + } + + override fun onBindViewHolder(holder: SingleViewHolder, position: Int) { + (holder.itemView as AppCompatImageButton).setOnClickListener { + listener.onEmojiSelected("", ( url ?: "" ) + stickers[position]) + } + Glide.with(holder.itemView) + .load(( url ?: "" ) + stickers[position]) + .thumbnail() + .into(holder.itemView) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_emoji_keyboard_sticker, parent, false) + return SingleViewHolder(view) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt new file mode 100644 index 0000000..b4517dc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -0,0 +1,153 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +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 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.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) + fun onTabRemoved(position: Int) + fun onStartDelete(viewHolder: RecyclerView.ViewHolder) + fun onStartDrag(viewHolder: RecyclerView.ViewHolder) + fun onActionChipClicked(tab: TabData, tabPosition: Int) + fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) +} + +class TabAdapter(private var data: List, + private val small: Boolean, + private val listener: ItemInteractionListener, + 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 + } else { + R.layout.item_tab_preference + } + val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, 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 { + listener.onTabAdded(tab) + } + } + holder.itemView.imageView?.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + listener.onStartDrag(holder) + true + } else { + false + } + } + holder.itemView.removeButton?.setOnClickListener { + listener.onTabRemoved(holder.adapterPosition) + } + if (holder.itemView.removeButton != null) { + holder.itemView.removeButton.isEnabled = removeButtonEnabled + ThemeUtils.setDrawableTint( + holder.itemView.context, + holder.itemView.removeButton.drawable, + (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) + ) + } + + if (!small) { + + if (tab.id == HASHTAG) { + holder.itemView.chipGroup.show() + + /* + * The chip group will always contain the actionChip (it is defined in the xml layout). + * The other dynamic chips are inserted in front of the actionChip. + * This code tries to reuse already added chips to reduce the number of Views created. + */ + tab.arguments.forEachIndexed { i, arg -> + + val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? + ?: Chip(context).apply { + holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1) + chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) + } + + chip.text = arg + + if(tab.arguments.size <= 1) { + chip.chipIcon = null + chip.setOnClickListener(null) + } else { + chip.setChipIconResource(R.drawable.ic_cancel_24dp) + chip.setOnClickListener { + listener.onChipClicked(tab, holder.adapterPosition, i) + } + } + } + + while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { + holder.itemView.chipGroup.removeViewAt(tab.arguments.size) + } + + holder.itemView.actionChip.setOnClickListener { + listener.onActionChipClicked(tab, holder.adapterPosition) + } + + } else { + holder.itemView.chipGroup.hide() + } + } + } + + override fun getItemCount() = data.size + + fun setRemoveButtonVisible(enabled: Boolean) { + if (removeButtonEnabled != enabled) { + removeButtonEnabled = enabled + notifyDataSetChanged() + } + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java new file mode 100644 index 0000000..0143cb4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -0,0 +1,164 @@ +/* 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.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.util.ArrayList; +import java.util.List; + +public class ThreadAdapter extends RecyclerView.Adapter { + private static final int VIEW_TYPE_STATUS = 0; + private static final int VIEW_TYPE_STATUS_DETAILED = 1; + + private List statuses; + private StatusDisplayOptions statusDisplayOptions; + private StatusActionListener statusActionListener; + private int detailedStatusPosition; + + public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) { + this.statusDisplayOptions = statusDisplayOptions; + this.statusActionListener = listener; + this.statuses = new ArrayList<>(); + detailedStatusPosition = RecyclerView.NO_POSITION; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_STATUS: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_STATUS_DETAILED: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status_detailed, parent, false); + return new StatusDetailedViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + StatusViewData.Concrete status = statuses.get(position); + if (position == detailedStatusPosition) { + StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder; + holder.setupWithStatus(status, statusActionListener, statusDisplayOptions); + } else { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + holder.setupWithStatus(status, statusActionListener, statusDisplayOptions); + } + } + + @Override + public int getItemViewType(int position) { + if (position == detailedStatusPosition) { + return VIEW_TYPE_STATUS_DETAILED; + } else { + return VIEW_TYPE_STATUS; + } + } + + @Override + public int getItemCount() { + return statuses.size(); + } + + public void setStatuses(List statuses) { + this.statuses.clear(); + this.statuses.addAll(statuses); + notifyDataSetChanged(); + } + + public void addItem(int position, StatusViewData.Concrete statusViewData) { + statuses.add(position, statusViewData); + notifyItemInserted(position); + } + + public void clearItems() { + int oldSize = statuses.size(); + statuses.clear(); + detailedStatusPosition = RecyclerView.NO_POSITION; + notifyItemRangeRemoved(0, oldSize); + } + + public void addAll(int position, List statuses) { + this.statuses.addAll(position, statuses); + notifyItemRangeInserted(position, statuses.size()); + } + + public void addAll(List statuses) { + int end = statuses.size(); + this.statuses.addAll(statuses); + notifyItemRangeInserted(end, statuses.size()); + } + + public void removeItem(int position) { + statuses.remove(position); + notifyItemRemoved(position); + } + + public void clear() { + statuses.clear(); + detailedStatusPosition = RecyclerView.NO_POSITION; + notifyDataSetChanged(); + } + + public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) { + statuses.set(position, status); + if (notifyAdapter) { + notifyItemChanged(position); + } + } + + @Nullable + public StatusViewData.Concrete getItem(int position) { + if (position >= 0 && position < statuses.size()) { + return statuses.get(position); + } else { + return null; + } + } + + public void setDetailedStatusPosition(int position) { + if (position != detailedStatusPosition + && detailedStatusPosition != RecyclerView.NO_POSITION) { + int prior = detailedStatusPosition; + detailedStatusPosition = position; + notifyItemChanged(prior); + } else { + detailedStatusPosition = position; + } + } + + public int getDetailedStatusPosition() { + return detailedStatusPosition; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java new file mode 100644 index 0000000..f5ab042 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -0,0 +1,151 @@ +/* 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.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.util.List; + +public final class TimelineAdapter extends RecyclerView.Adapter { + + public interface AdapterDataSource { + int getItemCount(); + + T getItemAt(int pos); + } + + private static final int VIEW_TYPE_STATUS = 0; + private static final int VIEW_TYPE_STATUS_MUTED = 1; + private static final int VIEW_TYPE_PLACEHOLDER = 2; + + private final AdapterDataSource dataSource; + private StatusDisplayOptions statusDisplayOptions; + private final StatusActionListener statusListener; + + public TimelineAdapter(AdapterDataSource dataSource, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener statusListener) { + this.dataSource = dataSource; + this.statusDisplayOptions = statusDisplayOptions; + this.statusListener = statusListener; + } + + public boolean getMediaPreviewEnabled() { + return statusDisplayOptions.mediaPreviewEnabled(); + } + + public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) { + this.statusDisplayOptions = statusDisplayOptions.copy( + statusDisplayOptions.animateAvatars(), + mediaPreviewEnabled, + statusDisplayOptions.useAbsoluteTime(), + statusDisplayOptions.showBotOverlay(), + statusDisplayOptions.useBlurhash(), + statusDisplayOptions.cardViewMode(), + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.renderStatusAsMention(), + statusDisplayOptions.hideStats() + ); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_STATUS: { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status, viewGroup, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_STATUS_MUTED: { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status_muted, viewGroup, false); + return new MutedStatusViewHolder(view); + } + case VIEW_TYPE_PLACEHOLDER: { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status_placeholder, viewGroup, false); + return new PlaceholderViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + bindViewHolder(viewHolder, position, null); + } + + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { + bindViewHolder(viewHolder, position, payloads); + } + + private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { + StatusViewData status = dataSource.getItemAt(position); + if (status instanceof StatusViewData.Placeholder) { + PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; + holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading()); + } else if (status instanceof StatusViewData.Concrete) { + StatusViewData.Concrete concrete = (StatusViewData.Concrete)status; + if(concrete.isMuted()) { + MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; + holder.setupWithStatus(concrete, statusListener, statusDisplayOptions, + payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); + } else { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + holder.setupWithStatus(concrete, statusListener, statusDisplayOptions, + payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); + } + } + } + + @Override + public int getItemCount() { + return dataSource.getItemCount(); + } + + @Override + public int getItemViewType(int position) { + if (dataSource.getItemAt(position) instanceof StatusViewData.Placeholder) { + return VIEW_TYPE_PLACEHOLDER; + } else { + StatusViewData.Concrete concrete = (StatusViewData.Concrete)dataSource.getItemAt(position); + if(concrete.isMuted()) { + return VIEW_TYPE_STATUS_MUTED; + } else { + return VIEW_TYPE_STATUS; + } + } + } + + @Override + public long getItemId(int position) { + return dataSource.getItemAt(position).getViewDataId(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java new file mode 100644 index 0000000..776fa14 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java @@ -0,0 +1,129 @@ +package com.keylesspalace.tusky.adapter; + +import android.view.*; +import android.util.*; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.google.android.flexbox.FlexboxLayoutManager; +import androidx.viewpager2.widget.ViewPager2; +import androidx.recyclerview.widget.*; +import androidx.emoji.widget.EmojiAppCompatButton; +import androidx.emoji.text.EmojiCompat; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.view.EmojiKeyboard; +import com.keylesspalace.tusky.util.Emojis; +import java.util.*; + +public class UnicodeEmojiAdapter + extends RecyclerView.Adapter + implements TabLayoutMediator.TabConfigurationStrategy, EmojiKeyboard.EmojiKeyboardAdapter { + + private String id; + private List recents; + private EmojiKeyboard.OnEmojiSelectedListener listener; + private RecyclerView recentsView; + + private final static float BUTTON_WIDTH_DP = 65.0f; // empirically found value :( + + public UnicodeEmojiAdapter(String id, EmojiKeyboard.OnEmojiSelectedListener listener) { + super(); + this.id = id; + this.listener = listener; + } + + @Override + public void onConfigureTab(TabLayout.Tab tab, int position) { + if(position == 0) { + tab.setIcon(R.drawable.ic_access_time); + } else { + tab.setText(Emojis.EMOJIS[position - 1][0]); + } + } + + @Override + public int getItemCount() { + return Emojis.EMOJIS.length + 1; + } + + @Override + public void onBindViewHolder(SingleViewHolder holder, int position) { + if(position == 0) { + recentsView = ((RecyclerView)holder.itemView); + recentsView.setAdapter(new UnicodeEmojiPageAdapter(recents, id, listener)); + } else { + ((RecyclerView)holder.itemView).setAdapter( + new UnicodeEmojiPageAdapter(Arrays.asList(Emojis.EMOJIS[position - 1]), id, listener)); + } + } + + @Override + public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_keyboard_page, parent, false); + SingleViewHolder holder = new SingleViewHolder(view); + + DisplayMetrics dm = parent.getContext().getResources().getDisplayMetrics(); + float wdp = dm.widthPixels / dm.density; + int rows = (int) (wdp / BUTTON_WIDTH_DP + 0.5); + + ((RecyclerView)view).setLayoutManager(new GridLayoutManager(view.getContext(), rows)); + return holder; + } + + @Override + public void onRecentsUpdate(Set set) { + recents = new ArrayList(set); + Collections.reverse(recents); + if(recentsView != null) + recentsView.getAdapter().notifyDataSetChanged(); + } + + private abstract class UnicodeEmojiBasePageAdapter extends RecyclerView.Adapter { + private final EmojiKeyboard.OnEmojiSelectedListener listener; + private final String id; + + public UnicodeEmojiBasePageAdapter(String id, EmojiKeyboard.OnEmojiSelectedListener listener) { + this.id = id; + this.listener = listener; + } + + abstract public String getEmoji(int position); + + @Override + public void onBindViewHolder(SingleViewHolder holder, int position) { + String emoji = getEmoji(position); + EmojiAppCompatButton btn = (EmojiAppCompatButton)holder.itemView; + + btn.setText(emoji); + btn.setOnClickListener(v -> listener.onEmojiSelected(id, emoji)); + } + + @Override + public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_keyboard_emoji, parent, false); + return new SingleViewHolder(view); + } + } + + private class UnicodeEmojiPageAdapter extends UnicodeEmojiBasePageAdapter { + private final List emojis; + + public UnicodeEmojiPageAdapter(List emojis, String id, EmojiKeyboard.OnEmojiSelectedListener listener) { + super(id, listener); + this.emojis = emojis; + } + + @Override + public int getItemCount() { + return emojis.size(); + } + + @Override + public String getEmoji(int position) { + return emojis.get(position); + } + } + +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt new file mode 100644 index 0000000..2b3a9b7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -0,0 +1,59 @@ +package com.keylesspalace.tusky.appstore + +import com.google.gson.Gson +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class CacheUpdater @Inject constructor( + eventHub: EventHub, + accountManager: AccountManager, + private val appDatabase: AppDatabase, + gson: Gson +) { + + private val disposable: Disposable + + init { + val timelineDao = appDatabase.timelineDao() + disposable = eventHub.events.subscribe { event -> + val accountId = accountManager.activeAccount?.id ?: return@subscribe + when (event) { + is FavoriteEvent -> + timelineDao.setFavourited(accountId, event.statusId, event.favourite) + is ReblogEvent -> + timelineDao.setReblogged(accountId, event.statusId, event.reblog) + is BookmarkEvent -> + timelineDao.setBookmarked(accountId, event.statusId, event.bookmark) + is UnfollowEvent -> + timelineDao.removeAllByUser(accountId, event.accountId) + is StatusDeletedEvent -> + timelineDao.delete(accountId, event.statusId) + is EmojiReactEvent -> { + val pleromaString = gson.toJson(event.newStatus.pleroma) + timelineDao.setPleroma(accountId, event.newStatus.id, pleromaString) + } + is PollVoteEvent -> { + val pollString = gson.toJson(event.poll) + timelineDao.setVoted(accountId, event.statusId, pollString) + } + } + } + } + + fun stop() { + this.disposable.dispose() + } + + fun clearForUser(accountId: Long) { + Single.fromCallable { + appDatabase.timelineDao().removeAllForAccount(accountId) + appDatabase.timelineDao().removeAllUsersForAccount(accountId) + } + .subscribeOn(Schedulers.io()) + .subscribe() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt new file mode 100644 index 0000000..0f469d7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -0,0 +1,28 @@ +package com.keylesspalace.tusky.appstore + +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.ChatMessage +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status + +data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable +data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable +data class BookmarkEvent(val statusId: String, val bookmark: Boolean) : Dispatchable +data class MuteConversationEvent(val statusId: String, val mute: Boolean) : Dispatchable +data class EmojiReactEvent(val newStatus: Status) : Dispatchable +data class UnfollowEvent(val accountId: String) : Dispatchable +data class BlockEvent(val accountId: String) : Dispatchable +data class MuteEvent(val accountId: String, val mute: Boolean) : Dispatchable +data class StatusDeletedEvent(val statusId: String) : Dispatchable +data class StatusPreviewEvent(val status: Status) : Dispatchable +data class StatusComposedEvent(val status: Status) : Dispatchable +data class StatusScheduledEvent(val status: Status) : Dispatchable +data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable +data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable +data class MainTabsChangedEvent(val newTabs: List) : Dispatchable +data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable +data class DomainMuteEvent(val instance: String): Dispatchable +data class ChatMessageDeliveredEvent(val chatMsg: ChatMessage) : Dispatchable +data class ChatMessageReceivedEvent(val chatMsg: ChatMessage) : Dispatchable +data class AnnouncementReadEvent(val announcementId: String): Dispatchable diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt new file mode 100644 index 0000000..ceaf513 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt @@ -0,0 +1,22 @@ +package com.keylesspalace.tusky.appstore + +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + +interface Event +interface Dispatchable : Event + +interface EventHub { + val events: Observable + fun dispatch(event: Dispatchable) +} + +object EventHubImpl : EventHub { + + private val eventsSubject = PublishSubject.create() + override val events: Observable = eventsSubject + + override fun dispatch(event: Dispatchable) { + eventsSubject.onNext(event) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..c0e6bdd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -0,0 +1,126 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +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.entity.Announcement +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.interfaces.LinkListener +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) + fun addReaction(announcementId: String, name: String) + fun removeReaction(announcementId: String, name: String) +} + +class AnnouncementAdapter( + private var items: List = emptyList(), + private val listener: AnnouncementActionListener, + private val wellbeingEnabled: Boolean = false +) : 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 onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) { + viewHolder.bind(items[position]) + } + + override fun getItemCount() = items.size + + fun updateList(items: List) { + 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 + ) + + 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/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt new file mode 100644 index 0000000..0b96b43 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -0,0 +1,180 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.PopupWindow +import androidx.activity.viewModels +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +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 { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } + + private lateinit var adapter: AnnouncementAdapter + + private val picker by lazy { EmojiPicker(this) } + private val pickerDialog by lazy { + PopupWindow(this) + .apply { + contentView = picker + isFocusable = true + setOnDismissListener { + currentAnnouncementId = null + } + } + } + private var currentAnnouncementId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_announcements) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = getString(R.string.title_announcements) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + announcementsList.setHasFixedSize(true) + announcementsList.layoutManager = LinearLayoutManager(this) + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + announcementsList.addItemDecoration(divider) + + val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + + adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled) + + announcementsList.adapter = adapter + + viewModel.announcements.observe(this) { + when (it) { + is Success -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + if (it.data.isNullOrEmpty()) { + errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) + errorMessageView.show() + } else { + errorMessageView.hide() + } + adapter.updateList(it.data ?: listOf()) + } + is Loading -> { + errorMessageView.hide() + } + is Error -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + refreshAnnouncements() + } + errorMessageView.show() + } + } + } + + viewModel.emojis.observe(this) { + picker.adapter = EmojiAdapter(it, this) + } + + viewModel.load() + progressBar.show() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun refreshAnnouncements() { + viewModel.load() + swipeRefreshLayout.isRefreshing = true + } + + override fun openReactionPicker(announcementId: String, target: View) { + currentAnnouncementId = announcementId + pickerDialog.showAsDropDown(target) + } + + override fun onEmojiSelected(shortcode: String) { + viewModel.addReaction(currentAnnouncementId!!, shortcode) + pickerDialog.dismiss() + } + + override fun addReaction(announcementId: String, name: String) { + viewModel.addReaction(announcementId, name) + } + + override fun removeReaction(announcementId: String, name: String) { + viewModel.removeReaction(announcementId, name) + } + + override fun onViewTag(tag: String?) { + val intent = Intent(this, ViewTagActivity::class.java) + intent.putExtra("hashtag", tag) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewAccount(id: String?) { + if (id != null) { + viewAccount(id) + } + } + + override fun onViewUrl(url: String?) { + if (url != null) { + viewUrl(url) + } + } + + companion object { + fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt new file mode 100644 index 0000000..b2aa778 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -0,0 +1,188 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.keylesspalace.tusky.appstore.AnnouncementReadEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.InstanceEntity +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.rxkotlin.Singles +import javax.inject.Inject + +class AnnouncementsViewModel @Inject constructor( + accountManager: AccountManager, + private val appDatabase: AppDatabase, + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : RxAwareViewModel() { + + private val announcementsMutable = MutableLiveData>>() + val announcements: LiveData>> = announcementsMutable + + private val emojisMutable = MutableLiveData>() + val emojis: LiveData> = emojisMutable + + init { + Singles.zip( + mastodonApi.getCustomEmojis(), + appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + .map> { Either.Left(it) } + .onErrorResumeNext( + mastodonApi.getInstance() + .map { Either.Right(it) } + ) + ) { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().maxTootChars, + either.asRight().pollLimits?.maxOptions, + either.asRight().pollLimits?.maxOptionChars, + either.asRight().version, + either.asRight().chatLimit + ) + } + .doOnSuccess { + appDatabase.instanceDao().insertOrReplace(it) + } + .subscribe({ + emojisMutable.postValue(it.emojiList) + }, { + Log.w(TAG, "Failed to get custom emojis.", it) + }) + .autoDispose() + } + + fun load() { + announcementsMutable.postValue(Loading()) + mastodonApi.listAnnouncements() + .subscribe({ + announcementsMutable.postValue(Success(it)) + it.filter { announcement -> !announcement.read } + .forEach { announcement -> + mastodonApi.dismissAnnouncement(announcement.id) + .subscribe( + { + eventHub.dispatch(AnnouncementReadEvent(announcement.id)) + }, + { throwable -> + Log.d(TAG, "Failed to mark announcement as read.", throwable) + } + ) + .autoDispose() + } + }, { + announcementsMutable.postValue(Error(cause = it)) + }) + .autoDispose() + } + + fun addReaction(announcementId: String, name: String) { + mastodonApi.addAnnouncementReaction(announcementId, name) + .subscribe({ + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true + ) + } else { + reaction + } + } + } else { + listOf( + *announcement.reactions.toTypedArray(), + emojis.value!!.find { emoji -> emoji.shortcode == name } + !!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl + ) + } + ) + } + ) + } else { + announcement + } + } + ) + ) + }, { + Log.w(TAG, "Failed to add reaction to the announcement.", it) + }) + .autoDispose() + } + + fun removeReaction(announcementId: String, name: String) { + mastodonApi.removeAnnouncementReaction(announcementId, name) + .subscribe({ + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> + if (reaction.name == name) { + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false + ) + } else { + null + } + } else { + reaction + } + } + ) + } else { + announcement + } + } + ) + ) + }, { + Log.w(TAG, "Failed to remove reaction from the announcement.", it) + }) + .autoDispose() + } + + companion object { + private const val TAG = "AnnouncementsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt new file mode 100644 index 0000000..c5ca662 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt @@ -0,0 +1,1125 @@ +package com.keylesspalace.tusky.components.chat + +import android.Manifest +import android.app.Activity +import android.app.ProgressDialog +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.KeyEvent +import android.view.MenuItem +import android.view.View +import android.widget.ImageButton +import android.widget.PopupMenu +import android.widget.Toast +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Chat +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.ChatMesssageOrPlaceholder +import com.keylesspalace.tusky.repository.ChatRepository +import com.keylesspalace.tusky.viewdata.ChatMessageViewData +import androidx.arch.core.util.Function +import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.core.view.inputmethod.InputContentInfoCompat +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.* +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.adapter.* +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.common.* +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.repository.Placeholder +import com.keylesspalace.tusky.repository.TimelineRequestMode +import com.keylesspalace.tusky.service.MessageToSend +import com.keylesspalace.tusky.service.ServiceClient +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.EmojiKeyboard +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 com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.activity_chat.* +import kotlinx.android.synthetic.main.toolbar_basic.toolbar +import java.io.File +import java.io.IOException +import java.lang.Exception +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class ChatActivity: BottomSheetActivity(), + Injectable, + ChatActionListener, + ComposeAutoCompleteAdapter.AutocompletionProvider, + EmojiKeyboard.OnEmojiSelectedListener, + OnEmojiSelectedListener, + InputConnectionCompat.OnCommitContentListener { + private val TAG = "ChatsActivity" // logging tag + private val LOAD_AT_ONCE = 30 + + @Inject + lateinit var eventHub: EventHub + @Inject + lateinit var api: MastodonApi + @Inject + lateinit var chatsRepo: ChatRepository + @Inject + lateinit var serviceClient: ServiceClient + @Inject + lateinit var viewModelFactory: ViewModelFactory + + @VisibleForTesting + val viewModel: ChatViewModel by viewModels { viewModelFactory } + @VisibleForTesting + var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT + + lateinit var adapter: ChatMessagesAdapter + + private val msgs = PairedList(Function { input -> + input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) ?: + ChatMessageViewData.Placeholder(input.asLeft().id, false) + }) + + private var bottomLoading = false + private var isNeedRefresh = false + private var didLoadEverythingBottom = false + private var initialUpdateFailed = false + private var haveStickers = false + + private lateinit var addMediaBehavior : BottomSheetBehavior<*> + private lateinit var emojiBehavior: BottomSheetBehavior<*> + private lateinit var stickerBehavior: BottomSheetBehavior<*> + + private var finishingUploadDialog: ProgressDialog? = null + private var photoUploadUri: Uri? = null + + private enum class FetchEnd { + TOP, BOTTOM, MIDDLE + } + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + Log.d(TAG, "onInserted") + adapter.notifyItemRangeInserted(position, count) + if (position == 0) { + recycler.scrollToPosition(0) + } + } + + override fun onRemoved(position: Int, count: Int) { + Log.d(TAG, "onRemoved") + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + Log.d(TAG, "onMoved") + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + Log.d(TAG, "onChanged") + adapter.notifyItemRangeChanged(position, count, payload) + } + } + + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + return oldItem.getViewDataId() == newItem.getViewDataId() + } + + override fun areContentsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Any? { + return if (oldItem.deepEquals(newItem)) { + //If items are equal - update timestamp only + listOf(ChatMessagesViewHolder.Key.KEY_CREATED) + } else // If items are different - update a whole view holder + null + } + } + + private val differ = AsyncListDiffer(listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build()) + + private val dataSource = object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): ChatMessageViewData { + return differ.currentList[pos] + } + } + + private lateinit var chatId : String + private lateinit var avatarUrl : String + private lateinit var displayName : String + private lateinit var username : String + private lateinit var emojis : ArrayList + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if(accountManager.activeAccount == null) { + throw Exception("No active account!") + } + + chatId = intent.getStringExtra(ID) ?: throw IllegalArgumentException("Can't open ChatActivity without chatId") + avatarUrl = intent.getStringExtra(AVATAR_URL) ?: throw IllegalArgumentException("Can't open ChatActivity without avatarUrl") + displayName = intent.getStringExtra(DISPLAY_NAME) ?: throw IllegalArgumentException("Can't open ChatActivity without displayName") + username = intent.getStringExtra(USERNAME) ?: throw IllegalArgumentException("Can't open ChatActivity without username") + emojis = intent.getParcelableArrayListExtra(EMOJIS) ?: throw IllegalArgumentException("Can't open ChatActivity without emojis") + + setContentView(R.layout.activity_chat) + setSupportActionBar(toolbar) + + subscribeToUpdates() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + viewModel.tryFetchStickers = preferences.getBoolean(PrefKeys.STICKERS, false) + viewModel.anonymizeNames = preferences.getBoolean(PrefKeys.ANONYMIZE_FILENAMES, false) + + setupHeader() + setupChat() + setupAttachment() + setupComposeField(savedInstanceState?.getString(MESSAGE_KEY)) + setupButtons() + + viewModel.setup() + + photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when(event) { + is ChatMessageDeliveredEvent -> { + if(event.chatMsg.chatId == chatId) { + onRefresh() + enableButton(attachmentButton, true, true) + enableButton(stickerButton, haveStickers, haveStickers) + + sending = false + enableSendButton() + } + } + is ChatMessageReceivedEvent -> { + if(event.chatMsg.chatId == chatId) { + onRefresh() + } + } + } + } + + tryCache() + } + + private fun setupHeader() { + supportActionBar?.run { + title = "" + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + loadAvatar(avatarUrl, chatAvatar, resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),true) + chatTitle.text = displayName.emojify(emojis, chatTitle, true) + chatUsername.text = username + } + + private fun setupChat() { + adapter = ChatMessagesAdapter(dataSource, this, accountManager.activeAccount!!.accountId) + + // TODO: a11y + recycler.setHasFixedSize(true) + val layoutManager = LinearLayoutManager(this) + layoutManager.reverseLayout = true + recycler.layoutManager = layoutManager + // recycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + recycler.adapter = adapter + } + + private fun setupAttachment() { + val onMediaPick = View.OnClickListener { view -> + val popup = PopupMenu(view.context, view) + val addCaptionId = 1 + val removeId = 2 + popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) + popup.menu.add(0, removeId, 0, R.string.action_remove) + popup.setOnMenuItemClickListener { menuItem -> + viewModel.media.value?.get(0)?.let { + when (menuItem.itemId) { + addCaptionId -> { + makeCaptionDialog(it.description, it.uri) { newDescription -> + viewModel.updateDescription(it.localId, newDescription) + } + } + removeId -> { + viewModel.removeMediaFromQueue(it) + } + } + } + true + } + popup.show() + } + + imageAttachment.setOnClickListener(onMediaPick) + textAttachment.setOnClickListener(onMediaPick) + } + + private fun setupComposeField(startingText: String?) { + editText.setOnCommitContentListener(this) + + editText.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } + + editText.setAdapter( + ComposeAutoCompleteAdapter(this)) + editText.setTokenizer(ComposeTokenizer()) + + editText.setText(startingText) + editText.setSelection(editText.length()) + + val mentionColour = editText.linkTextColors.defaultColor + highlightSpans(editText.text, mentionColour) + editText.afterTextChanged { editable -> + highlightSpans(editable, mentionColour) + enableSendButton() + } + + // 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) { + editText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + } + + private var sending = false + private fun enableSendButton() { + if(sending) + return + + val haveMedia = viewModel.media.value?.isNotEmpty() ?: false + val haveText = editText.text.isNotEmpty() + + enableButton(sendButton, haveMedia || haveText, haveMedia || haveText) + } + + override fun search(token: String): List { + return viewModel.searchAutocompleteSuggestions(token) + } + + /** This is for the fancy keyboards which can insert images and stuff. */ + override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { + // Verify the returned content's type is of the correct MIME type + val supported = inputContentInfo.description.hasMimeType("image/*") + + if(supported) { + val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + if(lacksPermission) { + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) + return false + } + } + pickMedia(inputContentInfo.contentUri, inputContentInfo) + return true + } + + return false + } + + private fun subscribeToUpdates() { + withLifecycleContext { + viewModel.instanceParams.observe { instanceData -> + maximumTootCharacters = instanceData.chatLimit + } + viewModel.instanceStickers.observe { stickers -> + if(stickers.isNotEmpty()) { + haveStickers = true + stickerButton.visibility = View.VISIBLE + enableButton(stickerButton, true, true) + stickerKeyboard.setupStickerKeyboard(this@ChatActivity, stickers) + } + } + viewModel.emoji.observe { setEmojiList(it) } + viewModel.media.observe { + val notHaveMedia = it.isEmpty() + + enableSendButton() + enableButton(attachmentButton, notHaveMedia, notHaveMedia) + enableButton(stickerButton, haveStickers && notHaveMedia, haveStickers && notHaveMedia) + + if(!notHaveMedia) { + val media = it[0] + + when(media.type) { + ComposeActivity.QueuedMedia.UNKNOWN -> { + textAttachment.visibility = View.VISIBLE + imageAttachment.visibility = View.GONE + + textAttachment.text = media.originalFileName + textAttachment.setChecked(!media.description.isNullOrEmpty()) + textAttachment.setProgress(media.uploadPercent) + } + ComposeActivity.QueuedMedia.AUDIO -> { + imageAttachment.visibility = View.VISIBLE + textAttachment.visibility = View.GONE + + imageAttachment.setChecked(!media.description.isNullOrEmpty()) + imageAttachment.setProgress(media.uploadPercent) + imageAttachment.setImageResource(R.drawable.ic_music_box_preview_24dp) + } + else -> { + imageAttachment.visibility = View.VISIBLE + textAttachment.visibility = View.GONE + + imageAttachment.setChecked(!media.description.isNullOrEmpty()) + imageAttachment.setProgress(media.uploadPercent) + + Glide.with(imageAttachment.context) + .load(media.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(imageAttachment) + } + } + + attachmentLayout.visibility = View.VISIBLE + } else { + attachmentLayout.visibility = View.GONE + } + } + viewModel.uploadError.observe { + displayTransientError(R.string.error_media_upload_sending) + } + } + } + + private fun setEmojiList(emojiList: List?) { + if (emojiList != null) { + emojiView.adapter = EmojiAdapter(emojiList, this@ChatActivity) + enableButton(emojiButton, true, emojiList.isNotEmpty()) + } + } + + private fun replaceTextAtCaret(text: CharSequence) { + // If you select "backward" in an editable, you get SelectionStart > SelectionEnd + val start = editText.selectionStart.coerceAtMost(editText.selectionEnd) + val end = editText.selectionStart.coerceAtLeast(editText.selectionEnd) + val textToInsert = if (start > 0 && !editText.text[start - 1].isWhitespace()) { + " $text" + } else { + text + } + editText.text.replace(start, end, textToInsert) + + // Set the cursor after the inserted text + editText.setSelection(start + text.length) + } + + override fun onEmojiSelected(shortcode: String) { + replaceTextAtCaret(":$shortcode: ") + } + + override fun onEmojiSelected(id: String, shortcode: String) { + Glide.with(this).asFile().load(shortcode).into( object : CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) { + displayTransientError(R.string.error_sticker_fetch) + } + + override fun onResourceReady(resource: File, transition: Transition?) { + val cut = shortcode.lastIndexOf('/') + val filename = if(cut != -1) shortcode.substring(cut + 1) else "unknown.png" + pickMedia(resource.toUri(), null, filename) + } + }) + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + private fun setupButtons() { + addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet) + emojiBehavior = BottomSheetBehavior.from(emojiView) + stickerBehavior = BottomSheetBehavior.from(stickerKeyboard) + + sendButton.setOnClickListener { onSendClicked() } + + attachmentButton.setOnClickListener { openPickDialog() } + emojiButton.setOnClickListener { showEmojis() } + if(viewModel.tryFetchStickers) { + stickerButton.setOnClickListener { showStickers() } + stickerButton.visibility = View.VISIBLE + enableButton(stickerButton, false, false) + } else { + stickerButton.visibility = View.GONE + } + + emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false) + + 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) + + val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } + actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) + + actionPhotoTake.setOnClickListener { initiateCameraApp() } + actionPhotoPick.setOnClickListener { onMediaPick() } + } + + private fun onSendClicked() { + val media = viewModel.getSingleMedia() + + serviceClient.sendChatMessage(MessageToSend( + editText.text.toString(), + media?.id, + media?.uri?.toString(), + accountManager.activeAccount!!.id, + this.chatId, + 0 + )) + + sending = true + editText.text.clear() + viewModel.media.value = listOf() + enableButton(sendButton, false, false) + enableButton(attachmentButton, false, false) + enableButton(stickerButton, false, false) + } + + private fun openPickDialog() { + if (addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun showEmojis() { + 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() + } else { + if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + } + } + + private fun showStickers() { + if (stickerBehavior.state == BottomSheetBehavior.STATE_HIDDEN || stickerBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + stickerBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun initiateCameraApp() { + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + + // We don't need to ask for permission in this case, because the used calls require + // android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was + // way before permission dialogues have been introduced. + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + if (intent.resolveActivity(packageManager) != null) { + val photoFile: File = try { + createNewImageFile(this) + } catch (ex: IOException) { + displayTransientError(R.string.error_media_upload_opening) + return + } + + // Continue only if the File was successfully created + photoUploadUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID + ".fileprovider", + photoFile) + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri) + startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT) + } + } + + private fun onMediaPick() { + addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + //Wait until bottom sheet is not collapsed and show next screen after + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.removeBottomSheetCallback(this) + if (ContextCompat.checkSelfPermission(this@ChatActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this@ChatActivity, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + } else { + initiateMediaPicking() + } + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initiateMediaPicking() + } else { + val bar = Snackbar.make(activityChat, R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT).apply { + + } + bar.setAction(R.string.action_retry) { onMediaPick()} + //necessary so snackbar is shown over everything + bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.show() + } + } + } + + private fun initiateMediaPicking() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + + intent.type = "*/*" // Pleroma allows anything + startActivityForResult(intent, MEDIA_PICK_RESULT) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { + pickMedia(intent.data!!) + } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { + pickMedia(photoUploadUri!!) + } + } + + private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { + button.isEnabled = clickable + ThemeUtils.setDrawableTint(this, button.drawable, + if (colorActive) android.R.attr.textColorTertiary + else R.attr.textColorDisabled) + } + + private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) { + withLifecycleContext { + viewModel.pickMedia(uri, filename ?: uri.toFileName(contentResolver)).observe { exceptionOrItem -> + + contentInfoCompat?.releasePermission() + + if(exceptionOrItem.isLeft()) { + val errorId = when (val exception = exceptionOrItem.asLeft()) { + is VideoSizeException -> { + R.string.error_video_upload_size + } + is MediaSizeException -> { + R.string.error_media_upload_size + } + is AudioSizeException -> { + R.string.error_audio_upload_size + } + is VideoOrImageException -> { + R.string.error_media_upload_image_or_video + } + else -> { + Log.d(TAG, "That file could not be opened", exception) + R.string.error_media_upload_opening + } + } + displayTransientError(errorId) + } + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri) + outState.putString(MESSAGE_KEY, editText.text.toString()) + super.onSaveInstanceState(outState) + } + + private fun displayTransientError(@StringRes stringId: Int) { + val bar = Snackbar.make(activityChat, stringId, Snackbar.LENGTH_LONG) + //necessary so snackbar is shown over everything + bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.show() + } + + private fun clearPlaceholdersForResponse(msgs: MutableList) { + msgs.removeAll { it.isLeft() } + } + + private fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + chatsRepo.getChatMessages(chatId, null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { msgs -> + if (msgs.size > 1) { + val mutableMsgs = msgs.toMutableList() + clearPlaceholdersForResponse(mutableMsgs) + this.msgs.clear() + this.msgs.addAll(mutableMsgs) + updateAdapter() + progressBar.visibility = View.GONE + // Request statuses including current top to refresh all of them + } + updateCurrent() + loadAbove() + } + } + + private fun updateCurrent() { + if (msgs.isEmpty()) { + return + } + + val topId = msgs.first { it.isRight() }.asRight().id + chatsRepo.getChatMessages(chatId, topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ messages -> + initialUpdateFailed = false + // When cached timeline is too old, we would replace it with nothing + if (messages.isNotEmpty()) { + // clear old cached statuses + if(this.msgs.isNotEmpty()) { + this.msgs.removeAll { + if(it.isRight()) { + val chat = it.asRight() + chat.id.length < topId.length || chat.id < topId + } else { + val placeholder = it.asLeft() + placeholder.id.length < topId.length || placeholder.id < topId + } + } + } + this.msgs.addAll(messages) + updateAdapter() + } + bottomLoading = false + }, { + initialUpdateFailed = true + // Indicate that we are not loading anymore + progressBar.visibility = View.GONE + }) + } + + private fun showNothing() { + messageView.visibility = View.VISIBLE + messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + + private fun loadAbove() { + var firstOrNull: String? = null + var secondOrNull: String? = null + for (i in msgs.indices) { + val msg = msgs[i] + if (msg.isRight()) { + firstOrNull = msg.asRight().id + if (i + 1 < msgs.size && msgs[i + 1].isRight()) { + secondOrNull = msgs[i + 1].asRight().id + } + break + } + } + if (firstOrNull != null) { + sendFetchMessagesRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) + } else { + sendFetchMessagesRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + private fun sendFetchMessagesRequest(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, + fetchEnd: FetchEnd, pos: Int) { + // allow getting old statuses/fallbacks for network only for for bottom loading + val mode = if (fetchEnd == FetchEnd.BOTTOM) { + TimelineRequestMode.ANY + } else { + TimelineRequestMode.NETWORK + } + chatsRepo.getChatMessages(chatId, maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( { result -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, + { onFetchTimelineFailure(Exception(it), fetchEnd, pos) }) + } + + private fun updateAdapter() { + Log.d(TAG, "updateAdapter") + differ.submitList(msgs.pairedCopy) + } + + private fun updateMessages(newMsgs: MutableList, fullFetch: Boolean) { + if (newMsgs.isEmpty()) { + updateAdapter() + return + } + if (msgs.isEmpty()) { + msgs.addAll(newMsgs) + } else { + val lastOfNew = newMsgs[newMsgs.size - 1] + val index = msgs.indexOf(lastOfNew) + if (index >= 0) { + msgs.subList(0, index).clear() + } + val newIndex = newMsgs.indexOf(msgs[0]) + if (newIndex == -1) { + if (index == -1 && fullFetch) { + newMsgs.findLast { it.isRight() }?.let { + val placeholderId = it.asRight().id.inc() + newMsgs.add(Either.Left(Placeholder(placeholderId))) + } + } + msgs.addAll(0, newMsgs) + } else { + msgs.addAll(0, newMsgs.subList(0, newIndex)) + } + } + // Remove all consecutive placeholders + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun removeConsecutivePlaceholders() { + for (i in 0 until msgs.size - 1) { + if (msgs[i].isLeft() && msgs[i + 1].isLeft()) { + msgs.removeAt(i) + } + } + } + + private fun replacePlaceholderWithMessages(newMsgs: MutableList, + fullFetch: Boolean, pos: Int) { + val placeholder = msgs[pos] + if (placeholder.isLeft()) { + msgs.removeAt(pos) + } + if (newMsgs.isEmpty()) { + updateAdapter() + return + } + if (fullFetch) { + newMsgs.add(placeholder) + } + msgs.addAll(pos, newMsgs) + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun addItems(newMsgs: List) { + if (newMsgs.isEmpty()) { + return + } + val last = msgs.findLast { it.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 && !newMsgs.contains(last)) { + msgs.addAll(newMsgs) + removeConsecutivePlaceholders() + updateAdapter() + } + } + + private fun onFetchTimelineSuccess(msgs: 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 = msgs.size >= LOAD_AT_ONCE + + when (fetchEnd) { + FetchEnd.TOP -> { + updateMessages(msgs, fullFetch) + + val last = msgs.indexOfFirst { it.isRight() } + + mastodonApi.markChatAsRead(chatId, msgs[last].asRight().id) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ + Log.d(TAG, "Marked new messages as read up to ${msgs[last].asRight().id}") + }, { + Log.d(TAG, "Failed to mark messages as read", it) + }) + } + FetchEnd.MIDDLE -> { + replacePlaceholderWithMessages(msgs, fullFetch, pos) + } + FetchEnd.BOTTOM -> { + if (this.msgs.isNotEmpty() && !this.msgs.last().isRight()) { + this.msgs.removeAt(this.msgs.size - 1) + updateAdapter() + } + + if (msgs.isNotEmpty() && !msgs.last().isRight()) { + // Removing placeholder if it's the last one from the cache + msgs.removeAt(msgs.size - 1) + } + + val oldSize = this.msgs.size + if (this.msgs.size > 1) { + addItems(msgs) + } else { + updateMessages(msgs, fullFetch) + } + + if (this.msgs.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 + } + } + } + updateBottomLoadingState(fetchEnd) + progressBar.visibility = View.GONE + if (this.msgs.size == 0) { + showNothing() + } else { + messageView.visibility = View.GONE + } + } + + private fun onRefresh() { + messageView.visibility = View.GONE + isNeedRefresh = false + + if (this.initialUpdateFailed) { + updateCurrent() + } + loadAbove() + } + + private fun onFetchTimelineFailure(exception: Exception, fetchEnd: FetchEnd, position: Int) { + if (fetchEnd == FetchEnd.MIDDLE && !msgs[position].isRight()) { + var placeholder = msgs[position].asLeftOrNull() + val newViewData: ChatMessageViewData + if (placeholder == null) { + val msg = msgs[position - 1].asRight() + val newId = msg.id.dec() + placeholder = Placeholder(newId) + } + newViewData = ChatMessageViewData.Placeholder(placeholder.id, false) + msgs.setPairedItem(position, newViewData) + updateAdapter() + } else if (msgs.isEmpty()) { + messageView.visibility = View.VISIBLE + if (exception is IOException) { + messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + progressBar.visibility = View.VISIBLE + onRefresh() + } + } else { + messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + progressBar.visibility = View.VISIBLE + onRefresh() + } + } + } + Log.e(TAG, "Fetch Failure: " + exception.message) + updateBottomLoadingState(fetchEnd) + progressBar.visibility = View.GONE + } + + private fun updateBottomLoadingState(fetchEnd: FetchEnd) { + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false + } + } + + override fun onLoadMore(position: Int) { + //check bounds before accessing list, + if (msgs.size >= position && position > 0) { + val fromChat = msgs[position - 1].asRightOrNull() + val toChat = msgs[position + 1].asRightOrNull() + if (fromChat == null || toChat == null) { + Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + return + } + + val maxMinusOne = if (msgs.size > position + 1 && msgs[position + 2].isRight()) msgs[position + 1].asRight().id else null + sendFetchMessagesRequest(fromChat.id, toChat.id, maxMinusOne, + FetchEnd.MIDDLE, position) + + val (id) = msgs[position].asLeft() + val newViewData = ChatMessageViewData.Placeholder(id, true) + msgs.setPairedItem(position, newViewData) + updateAdapter() + } else { + Log.e(TAG, "error loading more") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + Log.d(TAG, event.toString()) + if(event.action == KeyEvent.ACTION_DOWN) { + if (event.isCtrlPressed) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + // send message by pressing CTRL + ENTER + onSendClicked() + return true + } + } + + if (keyCode == KeyEvent.KEYCODE_BACK) { + onBackPressed() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + + override fun onBackPressed() { + // Acting like a teen: deliberately ignoring parent. + if (addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + stickerBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + return + } + + finish() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + 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(this) + val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false) + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_PAUSE) + .subscribe { updateAdapter() } + } + } + + override fun onViewAccount(id: String) { + viewAccount(id) + } + + override fun onViewUrl(url: String) { + viewUrl(url) + } + + override fun onViewTag(tag: String) { + val intent = Intent(this, ViewTagActivity::class.java) + intent.putExtra("hashtag", tag) + startActivity(intent) + } + + override fun onViewMedia(position: Int, view: View?) { + val attachment = msgs[position].asRight().attachment!! + + when(attachment.type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.AUDIO, Attachment.Type.IMAGE -> { + val intent = ViewMediaActivity.newIntent(this, attachment) + if(view != null) { + val url = attachment.url + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, url) + + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Attachment.Type.UNKNOWN -> { + viewUrl(attachment.url) + } + } + } + + companion object { + private const val MEDIA_PICK_RESULT = 1 + private const val MEDIA_TAKE_PHOTO_RESULT = 2 + private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 + private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" + private const val MESSAGE_KEY = "MESSAGE" + + fun getIntent(context: Context, chat: Chat) : Intent { + val intent = Intent(context, ChatActivity::class.java) + intent.putExtra(ID, chat.id) + intent.putExtra(AVATAR_URL, chat.account.avatar) + intent.putExtra(DISPLAY_NAME, chat.account.displayName ?: chat.account.localUsername) + intent.putParcelableArrayListExtra(EMOJIS, ArrayList(chat.account.emojis ?: emptyList())) + intent.putExtra(USERNAME, chat.account.username) + return intent + } + + const val ID = "id" + const val AVATAR_URL = "avatar_url" + const val DISPLAY_NAME = "display_name" + const val USERNAME = "username" + const val EMOJIS = "emojis" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatViewModel.kt new file mode 100644 index 0000000..d9a855f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatViewModel.kt @@ -0,0 +1,29 @@ +package com.keylesspalace.tusky.components.chat + +import com.keylesspalace.tusky.components.common.CommonComposeViewModel +import com.keylesspalace.tusky.components.common.MediaUploader +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.service.ServiceClient +import com.keylesspalace.tusky.util.* +import javax.inject.Inject + +open class ChatViewModel +@Inject constructor( + private val api: MastodonApi, + private val accountManager: AccountManager, + private val mediaUploader: MediaUploader, + private val serviceClient: ServiceClient, + private val saveTootHelper: SaveTootHelper, + private val db: AppDatabase +) : CommonComposeViewModel(api, accountManager, mediaUploader, db) { + + fun getSingleMedia() : ComposeActivity.QueuedMedia? { + return if(media.value?.isNotEmpty() == true) + media.value?.get(0) + else null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/common/CommonComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/common/CommonComposeViewModel.kt new file mode 100644 index 0000000..4e152b2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/common/CommonComposeViewModel.kt @@ -0,0 +1,382 @@ +/* Copyright 2019 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.common + +import android.net.Uri +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.InstanceEntity +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.Singles +import retrofit2.Response +import java.util.* +import javax.inject.Inject + +open class CommonComposeViewModel( + private val api: MastodonApi, + private val accountManager: AccountManager, + private val mediaUploader: MediaUploader, + private val db: AppDatabase +) : RxAwareViewModel() { + + protected val instance: MutableLiveData = MutableLiveData(null) + protected val nodeinfo: MutableLiveData = MutableLiveData(null) + protected val stickers: MutableLiveData> = MutableLiveData(emptyArray()) + val haveStickers: MutableLiveData = MutableLiveData(false) + var tryFetchStickers = false + var anonymizeNames = true + var hasNoAttachmentLimits = false + + val instanceParams: LiveData = instance.map { instance -> + ComposeInstanceParams( + maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, + chatLimit = instance?.chatLimit ?: DEFAULT_CHARACTER_LIMIT, + pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, + pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, + supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false + ) + } + val instanceMetadata: LiveData = nodeinfo.map { nodeinfo -> + val software = nodeinfo?.software?.name ?: "mastodon" + + if(software.equals("pleroma")) { + hasNoAttachmentLimits = true + ComposeInstanceMetadata( + software = "pleroma", + supportsMarkdown = nodeinfo?.metadata?.postFormats?.contains("text/markdown") ?: false, + supportsBBcode = nodeinfo?.metadata?.postFormats?.contains("text/bbcode") ?: false, + supportsHTML = nodeinfo?.metadata?.postFormats?.contains("text/html") ?: false, + videoLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_VIDEO_SIZE_LIMIT, + imageLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_IMAGE_SIZE_LIMIT + ) + } else if(software.equals("pixelfed")) { + ComposeInstanceMetadata( + software = "pixelfed", + supportsMarkdown = false, + supportsBBcode = false, + supportsHTML = false, + videoLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_VIDEO_SIZE_LIMIT, + imageLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_IMAGE_SIZE_LIMIT + ) + } else { + ComposeInstanceMetadata( + software = "mastodon", + supportsMarkdown = nodeinfo?.software?.version?.contains("+glitch") ?: false, + supportsBBcode = false, + supportsHTML = nodeinfo?.software?.version?.contains("+glitch") ?: false, + videoLimit = STATUS_VIDEO_SIZE_LIMIT, + imageLimit = STATUS_IMAGE_SIZE_LIMIT + ) + } + } + val instanceStickers: LiveData> = stickers // .map { stickers -> HashMap(stickers) } + + val emoji: MutableLiveData?> = MutableLiveData() + + val media = mutableLiveData>(listOf()) + val uploadError = MutableLiveData() + + protected val mediaToDisposable = mutableMapOf() + + init { + Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance -> + InstanceEntity( + instance = accountManager.activeAccount?.domain!!, + emojiList = emojis, + maximumTootCharacters = instance.maxTootChars, + maxPollOptions = instance.pollLimits?.maxOptions, + maxPollOptionLength = instance.pollLimits?.maxOptionChars, + version = instance.version, + chatLimit = instance.chatLimit + ) + } + .doOnSuccess { + db.instanceDao().insertOrReplace(it) + } + .onErrorResumeNext( + db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + ) + .subscribe ({ instanceEntity -> + emoji.postValue(instanceEntity.emojiList) + instance.postValue(instanceEntity) + }, { throwable -> + // this can happen on network error when no cached data is available + Log.w(TAG, "error loading instance data", throwable) + }) + .autoDispose() + + + api.getNodeinfoLinks().subscribe({ + links -> if(links.links.isNotEmpty()) { + api.getNodeinfo(links.links[0].href).subscribe({ + ni -> nodeinfo.postValue(ni) + }, { + err -> Log.d(TAG, "Failed to get nodeinfo", err) + }).autoDispose() + } + }, { err -> + Log.d(TAG, "Failed to get nodeinfo links", err) + }).autoDispose() + } + + fun pickMedia(uri: Uri, filename: String?): LiveData> { + // We are not calling .toLiveData() here because we don't want to stop the process when + // the Activity goes away temporarily (like on screen rotation). + val liveData = MutableLiveData>() + val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT + val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT + + mediaUploader.prepareMedia(uri, videoLimit, imageLimit, filename) + .map { (type, uri, size) -> + val mediaItems = media.value!! + if (!hasNoAttachmentLimits + && type != QueuedMedia.Type.IMAGE + && mediaItems.isNotEmpty() + && mediaItems[0].type == QueuedMedia.Type.IMAGE) { + throw VideoOrImageException() + } else { + addMediaToQueue(type, uri, size, filename ?: "unknown", anonymizeNames) + } + } + .subscribe({ queuedMedia -> + liveData.postValue(Either.Right(queuedMedia)) + }, { error -> + liveData.postValue(Either.Left(error)) + }) + .autoDispose() + return liveData + } + + private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String, anonymizeNames: Boolean): QueuedMedia { + val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename, + hasNoAttachmentLimits, anonymizeNames) + val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT + val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT + + media.value = media.value!! + mediaItem + mediaToDisposable[mediaItem.localId] = mediaUploader + .uploadMedia(mediaItem, videoLimit, imageLimit ) + .subscribe ({ event -> + val item = media.value?.find { it.localId == mediaItem.localId } + ?: return@subscribe + val newMediaItem = when (event) { + is UploadEvent.ProgressEvent -> + item.copy(uploadPercent = event.percentage) + is UploadEvent.FinishedEvent -> + item.copy(id = event.attachment.id, uploadPercent = -1) + } + synchronized(media) { + val mediaValue = media.value!! + val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } + media.postValue(if (index == -1) { + mediaValue + newMediaItem + } else { + mediaValue.toMutableList().also { it[index] = newMediaItem } + }) + } + }, { error -> + media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) + uploadError.postValue(error) + }) + return mediaItem + } + + protected fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) { + val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown", + hasNoAttachmentLimits, anonymizeNames, -1, id, description) + media.value = media.value!! + mediaItem + } + + fun removeMediaFromQueue(item: QueuedMedia) { + mediaToDisposable[item.localId]?.dispose() + media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } + } + + fun updateDescription(localId: Long, description: String): LiveData { + val newList = media.value!!.toMutableList() + val index = newList.indexOfFirst { it.localId == localId } + if (index != -1) { + newList[index] = newList[index].copy(description = description) + } + media.value = newList + val completedCaptioningLiveData = MutableLiveData() + media.observeForever(object : Observer> { + override fun onChanged(mediaItems: List) { + val updatedItem = mediaItems.find { it.localId == localId } + if (updatedItem == null) { + media.removeObserver(this) + } else if (updatedItem.id != null) { + api.updateMedia(updatedItem.id, description) + .subscribe({ + completedCaptioningLiveData.postValue(true) + }, { + completedCaptioningLiveData.postValue(false) + }) + .autoDispose() + media.removeObserver(this) + } + } + }) + return completedCaptioningLiveData + } + + fun searchAutocompleteSuggestions(token: String): List { + when (token[0]) { + '@' -> { + return try { + val acct = token.substring(1) + api.searchAccounts(query = acct, resolve = true, limit = 10) + .blockingGet() + .map { ComposeAutoCompleteAdapter.AccountResult(it) } + .filter { + it.account.username.startsWith(acct, ignoreCase = true) + } + } catch (e: Throwable) { + Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) + emptyList() + } + } + '#' -> { + return try { + api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) + .blockingGet() + .hashtags + .map { ComposeAutoCompleteAdapter.HashtagResult(it) } + } catch (e: Throwable) { + Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) + emptyList() + } + } + ':' -> { + val emojiList = emoji.value ?: return emptyList() + + val incomplete = token.substring(1).toLowerCase(Locale.ROOT) + val results = ArrayList() + val resultsInside = ArrayList() + for (emoji in emojiList) { + val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT) + if (shortcode.startsWith(incomplete)) { + results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) + } else if (shortcode.indexOf(incomplete, 1) != -1) { + resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) + } + } + if (results.isNotEmpty() && resultsInside.isNotEmpty()) { + results.add(ComposeAutoCompleteAdapter.ResultSeparator()) + } + results.addAll(resultsInside) + return results + } + else -> { + Log.w(TAG, "Unexpected autocompletion token: $token") + return emptyList() + } + } + } + + override fun onCleared() { + for (uploadDisposable in mediaToDisposable.values) { + uploadDisposable.dispose() + } + super.onCleared() + } + + private fun getStickers() { + if(!tryFetchStickers) + return + + api.getStickers().subscribe({ stickers -> + if (stickers.isNotEmpty()) { + haveStickers.postValue(true) + + val singles = mutableListOf>>() + + for(entry in stickers) { + val url = entry.value.removePrefix("/").removeSuffix("/") + "/pack.json"; + singles += api.getStickerPack(url) + } + + Single.zip(singles) { + it.map { + it as Response + it.body()!!.internal_url = it.raw().request.url.toString().removeSuffix("pack.json") + it.body()!! + } + }.onErrorReturn { + Log.d(TAG, "Failed to get sticker pack.json", it) + emptyList() + }.subscribe() { pack -> + if(pack.isNotEmpty()) { + val array = pack.toTypedArray() + array.sort() + this.stickers.postValue(array) + } + }.autoDispose() + } + }, { + err -> Log.d(TAG, "Failed to get sticker.json", err) + }).autoDispose() + } + + fun setup() { + getStickers() // early as possible + } + + private companion object { + const val TAG = "CCVM" + } + +} + +fun mutableLiveData(default: T) = MutableLiveData().apply { value = default } + +const val DEFAULT_CHARACTER_LIMIT = 500 +const val DEFAULT_MAX_OPTION_COUNT = 4 +const val DEFAULT_MAX_OPTION_LENGTH = 25 +const val STATUS_VIDEO_SIZE_LIMIT : Long = 41943040 // 40MiB +const val STATUS_IMAGE_SIZE_LIMIT : Long = 8388608 // 8MiB + +data class ComposeInstanceParams( + val maxChars: Int, + val chatLimit: Int, + val pollMaxOptions: Int, + val pollMaxLength: Int, + val supportsScheduled: Boolean +) + +data class ComposeInstanceMetadata( + val software: String, + val supportsMarkdown: Boolean, + val supportsBBcode: Boolean, + val supportsHTML: Boolean, + val videoLimit: Long, + val imageLimit: Long +) + +/** + * Throw when trying to add an image when video is already present or the other way around + */ +class VideoOrImageException : Exception() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java new file mode 100644 index 0000000..c42a5d3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java @@ -0,0 +1,154 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.common; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; + +import com.keylesspalace.tusky.util.IOUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize; +import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation; +import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap; + +/** + * Reduces the file size of images to fit under a given limit by resizing them, maintaining both + * aspect ratio and orientation. + */ +public class DownsizeImageTask extends AsyncTask { + private int sizeLimit; + private ContentResolver contentResolver; + private Listener listener; + private File tempFile; + + /** + * @param sizeLimit the maximum number of bytes each image can take + * @param contentResolver to resolve the specified images' URIs + * @param tempFile the file where the result will be stored + * @param listener to whom the results are given + */ + public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) { + this.sizeLimit = sizeLimit; + this.contentResolver = contentResolver; + this.tempFile = tempFile; + this.listener = listener; + } + + @Override + protected Boolean doInBackground(Uri... uris) { + boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile); + if (isCancelled()) { + return false; + } + return result; + } + + @Override + protected void onPostExecute(Boolean successful) { + if (successful) { + listener.onSuccess(tempFile); + } else { + listener.onFailure(); + } + super.onPostExecute(successful); + } + + public static boolean resize(Uri[] uris, long sizeLimit, ContentResolver contentResolver, + File tempFile) { + for (Uri uri : uris) { + InputStream inputStream; + try { + inputStream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + return false; + } + // Initially, just get the image dimensions. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + IOUtils.closeQuietly(inputStream); + // Get EXIF data, for orientation info. + int orientation = getImageOrientation(uri, contentResolver); + /* Unfortunately, there isn't a determined worst case compression ratio for image + * formats. So, the only way to tell if they're too big is to compress them and + * test, and keep trying at smaller sizes. The initial estimate should be good for + * many cases, so it should only iterate once, but the loop is used to be absolutely + * sure it gets downsized to below the limit. */ + int scaledImageSize = 1024; + do { + OutputStream stream; + try { + stream = new FileOutputStream(tempFile); + } catch (FileNotFoundException e) { + return false; + } + try { + inputStream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + return false; + } + options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize); + options.inJustDecodeBounds = false; + Bitmap scaledBitmap; + try { + scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options); + } catch (OutOfMemoryError error) { + return false; + } finally { + IOUtils.closeQuietly(inputStream); + } + if (scaledBitmap == null) { + return false; + } + Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation); + if (reorientedBitmap == null) { + scaledBitmap.recycle(); + return false; + } + Bitmap.CompressFormat format; + /* It's not likely the user will give transparent images over the upload limit, but + * if they do, make sure the transparency is retained. */ + if (!reorientedBitmap.hasAlpha()) { + format = Bitmap.CompressFormat.JPEG; + } else { + format = Bitmap.CompressFormat.PNG; + } + reorientedBitmap.compress(format, 85, stream); + reorientedBitmap.recycle(); + scaledImageSize /= 2; + } while (tempFile.length() > sizeLimit); + } + return true; + } + + /** + * Used to communicate the results of the task. + */ + public interface Listener { + void onSuccess(File file); + + void onFailure(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/common/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/common/MediaUploader.kt new file mode 100644 index 0000000..a2f06d2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/common/MediaUploader.kt @@ -0,0 +1,261 @@ +/* Copyright 2019 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.common + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.provider.OpenableColumns +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.ProgressRequestBody +import com.keylesspalace.tusky.util.* +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.* + +sealed class UploadEvent { + data class ProgressEvent(val percentage: Int) : UploadEvent() + data class FinishedEvent(val attachment: Attachment) : UploadEvent() +} + +fun createNewImageFile(context: Context, name: String = "Photo"): File { + // Create an image file name + val randomId = randomAlphanumericString(4) + val imageFileName = "${name}_${randomId}" + val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile( + imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ) +} + +data class PreparedMedia(val type: Int, val uri: Uri, val size: Long) + +interface MediaUploader { + fun prepareMedia(inUri: Uri, videoLimit: Long, imageLimit: Long, filename: String?): Single + fun uploadMedia(media: QueuedMedia, videoLimit: Long, imageLimit: Long): Observable +} + +class AudioSizeException : Exception() +class VideoSizeException : Exception() +class MediaSizeException : Exception() +class MediaTypeException : Exception() +class CouldNotOpenFileException : Exception() + +class MediaUploaderImpl( + private val context: Context, + private val mastodonApi: MastodonApi +) : MediaUploader { + override fun uploadMedia(media: QueuedMedia, videoLimit: Long, imageLimit: Long): Observable { + return Observable + .fromCallable { + if (shouldResizeMedia(media, imageLimit)) { + downsize(media, imageLimit) + } else media + } + .switchMap { upload(it) } + .subscribeOn(Schedulers.io()) + } + + private fun getMimeTypeAndSuffixFromFilenameOrUri(uri: Uri, filename: String?) : Pair { + val mimeType = contentResolver.getType(uri) + return if(mimeType == null && filename != null) { + val extension = filename.substringAfterLast('.', "tmp") + Pair(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension), ".$extension") + } else { + Pair(mimeType, "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")) + } + } + + override fun prepareMedia(inUri: Uri, videoLimit: Long, imageLimit: Long, filename: String?): Single { + return Single.fromCallable { + var mediaSize = getMediaSize(contentResolver, inUri) + var uri = inUri + val (mimeType, suffix) = getMimeTypeAndSuffixFromFilenameOrUri(uri, filename) + + try { + contentResolver.openInputStream(inUri).use { input -> + if (input == null) { + Log.w(TAG, "Media input is null") + uri = inUri + return@use + } + val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) + FileOutputStream(file.absoluteFile).use { out -> + input.copyTo(out) + uri = FileProvider.getUriForFile(context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file) + mediaSize = getMediaSize(contentResolver, uri) + } + + } + } catch (e: IOException) { + Log.w(TAG, e) + uri = inUri + } + if (mediaSize == MEDIA_SIZE_UNKNOWN) { + throw CouldNotOpenFileException() + } + + if (mimeType != null) { + val topLevelType = mimeType.substring(0, mimeType.indexOf('/')) + when (topLevelType) { + "video" -> { + if (mediaSize > videoLimit) { + throw VideoSizeException() + } + PreparedMedia(QueuedMedia.VIDEO, uri, mediaSize) + } + "image" -> { + PreparedMedia(QueuedMedia.IMAGE, uri, mediaSize) + } + "audio" -> { + if (mediaSize > videoLimit) { // TODO: CHANGE!!11 + throw AudioSizeException() + } + PreparedMedia(QueuedMedia.AUDIO, uri, mediaSize) + } + else -> { + if (mediaSize > videoLimit) { + throw MediaSizeException() + } + PreparedMedia(QueuedMedia.UNKNOWN, uri, mediaSize) + // throw MediaTypeException() + } + } + } else { + throw MediaTypeException() + } + } + } + + private val contentResolver = context.contentResolver + + private fun upload(media: QueuedMedia): Observable { + return Observable.create { emitter -> + var (mimeType, fileExtension) = getMimeTypeAndSuffixFromFilenameOrUri(media.uri, media.originalFileName) + val filename = if(!media.anonymizeFileName) media.originalFileName else + String.format("%s_%s_%s%s", + context.getString(R.string.app_name), + Date().time.toString(), + randomAlphanumericString(10), + fileExtension) + + val stream = contentResolver.openInputStream(media.uri) + + if (mimeType == null) mimeType = "multipart/form-data" + + var lastProgress = -1 + val fileBody = ProgressRequestBody(stream, media.mediaSize, + mimeType.toMediaTypeOrNull()) { percentage -> + if (percentage != lastProgress) { + emitter.onNext(UploadEvent.ProgressEvent(percentage)) + } + lastProgress = percentage + } + + val body = MultipartBody.Part.createFormData("file", filename, fileBody) + + val description = if (media.description != null) { + MultipartBody.Part.createFormData("description", media.description) + } else { + null + } + + val uploadDisposable = mastodonApi.uploadMedia(body, description) + .subscribe({ attachment -> + emitter.onNext(UploadEvent.FinishedEvent(attachment)) + emitter.onComplete() + }, { e -> + emitter.onError(e) + }) + + // Cancel the request when our observable is cancelled + emitter.setDisposable(uploadDisposable) + } + } + + private fun downsize(media: QueuedMedia, imageLimit: Long): QueuedMedia { + val file = createNewImageFile(context, media.originalFileName) + DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file) + return media.copy(uri = file.toUri(), mediaSize = file.length()) + } + + private fun shouldResizeMedia(media: QueuedMedia, imageLimit: Long): Boolean { + // resize only images + if(media.type == QueuedMedia.Type.IMAGE) { + // resize when exceed image limit + if(media.mediaSize >= imageLimit) + return true + + // don't resize when instance permits any image resolution(Pleroma) + if(media.noChanges) + return false + + // resize when exceed pixel limit + if(getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) + return true + } + + return false + } + + private companion object { + private const val TAG = "MediaUploaderImpl" + private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels + } +} + +fun Uri.toFileName(contentResolver: ContentResolver? = null): String { + var result: String = "unknown" + + if(scheme.equals("content") && contentResolver != null) { + val cursor = contentResolver.query(this, null, null, null, null) + cursor?.use{ + if(it.moveToFirst()) { + result = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } + } + + if(result.equals("unknown")) { + path?.let { + result = it + val cut = result.lastIndexOf('/') + if (cut != -1) { + result = result.substring(cut + 1) + } + } + } + return result +} \ No newline at end of file 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 new file mode 100644 index 0000000..5539d10 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -0,0 +1,1370 @@ +/* Copyright 2019 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.compose + +import android.Manifest +import android.app.Activity +import android.app.ProgressDialog +import android.app.TimePickerDialog +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.provider.MediaStore +import android.provider.OpenableColumns +import android.text.TextUtils +import android.util.Log +import android.view.KeyEvent +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.activity.viewModels +import androidx.annotation.ColorInt +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.core.view.inputmethod.InputContentInfoCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionManager +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.common.* +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.db.AccountEntity +import com.keylesspalace.tusky.db.DraftAttachment +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.EmojiKeyboard +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.android.synthetic.main.activity_compose.* +import java.io.File +import java.io.IOException +import java.util.* +import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min +import me.thanel.markdownedit.MarkdownEdit +import io.reactivex.android.schedulers.AndroidSchedulers +import com.uber.autodispose.android.lifecycle.autoDispose + +class ComposeActivity : BaseActivity(), + ComposeOptionsListener, + ComposeAutoCompleteAdapter.AutocompletionProvider, + OnEmojiSelectedListener, + Injectable, + InputConnectionCompat.OnCommitContentListener, + TimePickerDialog.OnTimeSetListener, + EmojiKeyboard.OnEmojiSelectedListener { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + @Inject + lateinit var eventHub: EventHub + + private lateinit var composeOptionsBehavior: BottomSheetBehavior<*> + private lateinit var addMediaBehavior: BottomSheetBehavior<*> + private lateinit var emojiBehavior: BottomSheetBehavior<*> + private lateinit var scheduleBehavior: BottomSheetBehavior<*> + private lateinit var stickerBehavior: BottomSheetBehavior<*> + private lateinit var previewBehavior: BottomSheetBehavior<*> + + // this only exists when a status is trying to be sent, but uploads are still occurring + private var finishingUploadDialog: ProgressDialog? = null + private var photoUploadUri: Uri? = null + + @VisibleForTesting + var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT + + @VisibleForTesting + val viewModel: ComposeViewModel by viewModels { viewModelFactory } + private var suggestFormattingSyntax: String = "text/markdown" + + 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) + + setupActionBar() + // do not do anything when not logged in, activity will be finished in super.onCreate() anyway + val activeAccount = accountManager.activeAccount ?: return + + viewModel.tryFetchStickers = preferences.getBoolean(PrefKeys.STICKERS, false) + viewModel.anonymizeNames = preferences.getBoolean(PrefKeys.ANONYMIZE_FILENAMES, false) + setupAvatar(preferences, activeAccount) + val mediaAdapter = MediaPreviewAdapter( + this, + onAddCaption = { item -> + makeCaptionDialog(item.description, item.uri) { newDescription -> + viewModel.updateDescription(item.localId, newDescription) + } + }, + onRemove = this::removeMediaFromQueue + ) + composeMediaPreviewBar.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + composeMediaPreviewBar.adapter = mediaAdapter + composeMediaPreviewBar.itemAnimator = null + + // set before subscribing to updates to not accidentally catch it + viewModel.formattingSyntax.value = activeAccount.defaultFormattingSyntax + + subscribeToUpdates(mediaAdapter, activeAccount) + setupButtons() + + photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) + + /* If the composer is started up as a reply to another post, override the "starting" state + * based on what the intent from the reply request passes. */ + + val composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) + + if (!composeOptions?.formattingSyntax.isNullOrEmpty()) { + suggestFormattingSyntax = composeOptions?.formattingSyntax!! + } else { + suggestFormattingSyntax = activeAccount.defaultFormattingSyntax + } + + viewModel.setup(composeOptions) + setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) + val tootText = composeOptions?.tootText + if (!tootText.isNullOrEmpty()) { + composeEditField.setText(tootText) + } + + if (!composeOptions?.scheduledAt.isNullOrEmpty()) { + composeScheduleView.setDateTime(composeOptions?.scheduledAt) + } + + setupComposeField(viewModel.startingText) + setupContentWarningField(composeOptions?.contentWarning) + setupPollView() + applyShareIntent(intent, savedInstanceState) + viewModel.setupComplete.value = true + + stickerKeyboard.isSticky = true + + eventHub.events.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when(event) { + is StatusPreviewEvent -> onStatusPreviewReady(event.status) + } + } + } + + private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + /* Get incoming images being sent through a share action from another app. Only do this + * when savedInstanceState is null, otherwise both the images from the intent and the + * instance state will be re-queued. */ + intent.type?.also { type -> + if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { + when (intent.action) { + Intent.ACTION_SEND -> { + intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> + pickMedia(uri) + } + } + Intent.ACTION_SEND_MULTIPLE -> { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.forEach { uri -> + pickMedia(uri) + } + } + } + } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { + + val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) + val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty() + val shareBody = if (!subject.isNullOrBlank() && subject !in text) { + subject + '\n' + text + } else { + text + } + + if (shareBody.isNotBlank()) { + val start = composeEditField.selectionStart.coerceAtLeast(0) + val end = composeEditField.selectionEnd.coerceAtLeast(0) + val left = min(start, end) + val right = max(start, end) + composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + // move edittext cursor to first when shareBody parsed + composeEditField.text.insert(0, "\n") + composeEditField.setSelection(0) + } + } + } + } + } + + private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { + if (replyingStatusAuthor != null) { + composeReplyView.show() + 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) + + composeReplyView.setOnClickListener { + TransitionManager.beginDelayedTransition(composeReplyContentView.parent as ViewGroup) + + if (composeReplyContentView.isVisible) { + composeReplyContentView.hide() + composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) + } else { + 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) + } + } + } + replyingStatusContent?.let { composeReplyContentView.text = it } + } + + private fun setupContentWarningField(startingContentWarning: String?) { + if (startingContentWarning != null) { + composeContentWarningField.setText(startingContentWarning) + } + composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } + } + + private fun setupComposeField(startingText: String?) { + composeEditField.setOnCommitContentListener(this) + + composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } + + composeEditField.setAdapter( + ComposeAutoCompleteAdapter(this)) + composeEditField.setTokenizer(ComposeTokenizer()) + + composeEditField.setText(startingText) + composeEditField.setSelection(composeEditField.length()) + + val mentionColour = composeEditField.linkTextColors.defaultColor + highlightSpans(composeEditField.text, mentionColour) + composeEditField.afterTextChanged { editable -> + highlightSpans(editable, mentionColour) + updateVisibleCharactersLeft() + } + + // 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) + } + } + + private fun reenableAttachments() { + // in case of we already had disabled attachments + // but got information about extension later + enableButton(composeAddMediaButton, true, true) + enablePollButton(true) + } + + @VisibleForTesting + var supportedFormattingSyntax = arrayListOf() + + private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter, activeAccount: AccountEntity) { + withLifecycleContext { + viewModel.instanceParams.observe { instanceData -> + maximumTootCharacters = instanceData.maxChars + updateVisibleCharactersLeft() + composeScheduleButton.visible(instanceData.supportsScheduled) + } + viewModel.instanceMetadata.observe { instanceData -> + if(instanceData.supportsMarkdown) { + supportedFormattingSyntax.add("text/markdown") + } + + if(instanceData.supportsBBcode) { + supportedFormattingSyntax.add("text/bbcode") + } + + if(instanceData.supportsHTML) { + supportedFormattingSyntax.add("text/html") + } + + if(supportedFormattingSyntax.size != 0) { + composeFormattingSyntax.visible(true) + + val supportsPrefferedSyntax = supportedFormattingSyntax.contains(viewModel.formattingSyntax.value!!) + + if(!supportsPrefferedSyntax) { + suggestFormattingSyntax = if(supportedFormattingSyntax.contains(activeAccount.defaultFormattingSyntax)) + activeAccount.defaultFormattingSyntax + else supportedFormattingSyntax[0] + + viewModel.formattingSyntax.value = "" + } + } + + if(instanceData.software == "pleroma") { + composePreviewButton.visibility = View.VISIBLE + reenableAttachments() + } + } + viewModel.haveStickers.observe { haveStickers -> + if (haveStickers) { + composeStickerButton.visibility = View.VISIBLE + } + } + viewModel.instanceStickers.observe { stickers -> + /*for(sticker in stickers) + Log.d(TAG, "Found sticker pack: %s from %s".format(sticker.title, sticker.internal_url))*/ + + if(stickers.isNotEmpty()) { + composeStickerButton.visibility = View.VISIBLE + enableButton(composeStickerButton, true, true) + stickerKeyboard.setupStickerKeyboard(this@ComposeActivity, stickers) + } + } + viewModel.emoji.observe { emoji -> setEmojiList(emoji) } + combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> + updateSensitiveMediaToggle(markSensitive, showContentWarning) + showContentWarning(showContentWarning) + }.subscribe() + viewModel.statusVisibility.observe { visibility -> + setStatusVisibility(visibility) + } + viewModel.media.observe { media -> + mediaAdapter.submitList(media) + if (media.size != mediaCount) { + mediaCount = media.size + 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) + } + viewModel.scheduledAt.observe { scheduledAt -> + if (scheduledAt == null) { + composeScheduleView.resetSchedule() + } else { + composeScheduleView.setDateTime(scheduledAt) + } + updateScheduleButton() + } + combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> + if(!viewModel.hasNoAttachmentLimits) { + val active = poll == null && media!!.size != 4 + && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) + enableButton(composeAddMediaButton, active, active) + enablePollButton(media.isNullOrEmpty()) + } + }.subscribe() + viewModel.uploadError.observe { + displayTransientError(R.string.error_media_upload_sending) + } + viewModel.setupComplete.observe { + // Focus may have changed during view model setup, ensure initial focus is on the edit field + composeEditField.requestFocus() + } + viewModel.formattingSyntax.observe { + if(it.isEmpty()) { + enableFormattingSyntaxButton(suggestFormattingSyntax, false) + setIconForSyntax(suggestFormattingSyntax, false) + } else { + val enable = it == suggestFormattingSyntax + + enableFormattingSyntaxButton(it, enable) + setIconForSyntax(it, enable) + } + } + } + } + + private fun setupButtons() { + composeOptionsBottomSheet.listener = this + + composeOptionsBehavior = BottomSheetBehavior.from(composeOptionsBottomSheet) + addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet) + scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) + emojiBehavior = BottomSheetBehavior.from(emojiView) + stickerBehavior = BottomSheetBehavior.from(stickerKeyboard) + previewBehavior = BottomSheetBehavior.from(previewScroll) + + enableButton(composeEmojiButton, clickable = false, colorActive = false) + enableButton(composeStickerButton, false, false) + + // Setup the interface buttons. + composeTootButton.setOnClickListener { onSendClicked(false) } + composePreviewButton.setOnClickListener { onSendClicked(true) } + composeAddMediaButton.setOnClickListener { openPickDialog() } + composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } + composeContentWarningButton.setOnClickListener { onContentWarningChanged() } + composeEmojiButton.setOnClickListener { showEmojis() } + composeHideMediaButton.setOnClickListener { toggleHideMedia() } + composeScheduleButton.setOnClickListener { onScheduleClick() } + composeScheduleView.setResetOnClickListener { resetSchedule() } + composeFormattingSyntax.setOnClickListener { toggleFormattingMode() } + composeFormattingSyntax.setOnLongClickListener { selectFormattingSyntax() } + composeStickerButton.setOnClickListener { showStickers() } + atButton.setOnClickListener { atButtonClicked() } + hashButton.setOnClickListener { hashButtonClicked() } + codeButton.setOnClickListener { codeButtonClicked() } + linkButton.setOnClickListener { linkButtonClicked() } + strikethroughButton.setOnClickListener { strikethroughButtonClicked() } + italicButton.setOnClickListener { italicButtonClicked() } + boldButton.setOnClickListener { boldButtonClicked() } + + 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) + + val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } + 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) + + actionPhotoTake.setOnClickListener { initiateCameraApp() } + actionPhotoPick.setOnClickListener { onMediaPick() } + addPollTextActionTextView.setOnClickListener { openPollDialog() } + } + + private fun setupActionBar() { + setSupportActionBar(toolbar) + supportActionBar?.run { + title = null + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_close_24dp) + } + + } + + private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) { + val actionBarSizeAttr = intArrayOf(R.attr.actionBarSize) + val a = obtainStyledAttributes(null, actionBarSizeAttr) + val avatarSize = a.getDimensionPixelSize(0, 1) + a.recycle() + + val animateAvatars = preferences.getBoolean("animateGifAvatars", false) + loadAvatar( + activeAccount.profilePictureUrl, + composeAvatar, + avatarSize / 8, + animateAvatars + ) + 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()) { + " $text" + } else { + text + } + composeEditField.text.replace(start, end, textToInsert) + + // Set the cursor after the inserted text + composeEditField.setSelection(start + text.length) + } + + private fun enableFormattingSyntaxButton(syntax: String, enable: Boolean) { + val stringId = when(syntax) { + "text/html" -> R.string.action_html + "text/bbcode" -> R.string.action_bbcode + else -> R.string.action_markdown + } + + val actionStringId = if(enable) R.string.action_disable_formatting_syntax else R.string.action_enable_formatting_syntax + val tooltipText = getString(actionStringId).format(stringId) + + composeFormattingSyntax.contentDescription = tooltipText + + @ColorInt val color = ThemeUtils.getColor(this, if(enable) R.attr.colorPrimary else android.R.attr.textColorTertiary); + composeFormattingSyntax.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); + + enableMarkdownWYSIWYGButtons(enable); + } + + private fun setIconForSyntax(syntax: String, enable: Boolean) { + val drawableId = when(syntax) { + "text/html" -> R.drawable.ic_html_24dp + "text/bbcode" -> R.drawable.ic_bbcode_24dp + else -> R.drawable.ic_markdown + } + + suggestFormattingSyntax = if(drawableId == R.drawable.ic_markdown) "text/markdown" else syntax + composeFormattingSyntax.setImageResource(drawableId) + enableFormattingSyntaxButton(syntax, enable) + } + + private fun toggleFormattingMode() { + if(viewModel.formattingSyntax.value!! == suggestFormattingSyntax) { + viewModel.formattingSyntax.value = "" + } else { + viewModel.formattingSyntax.value = suggestFormattingSyntax + } + } + + private fun selectFormattingSyntax() : Boolean { + val menu = PopupMenu(this, composeFormattingSyntax) + val plaintextId = 0 + val markdownId = 1 + val bbcodeId = 2 + val htmlId = 3 + menu.menu.add(0, plaintextId, 0, R.string.action_plaintext) + if(viewModel.instanceMetadata.value?.supportsMarkdown ?: false) + menu.menu.add(0, markdownId, 0, R.string.action_markdown) + + if(viewModel.instanceMetadata.value?.supportsBBcode ?: false) + menu.menu.add(0, bbcodeId, 0, R.string.action_bbcode) + + if(viewModel.instanceMetadata.value?.supportsHTML ?: false) + menu.menu.add(0, htmlId, 0, R.string.action_html) + + menu.setOnMenuItemClickListener { menuItem -> + val choose = when (menuItem.itemId) { + markdownId -> "text/markdown" + bbcodeId -> "text/bbcode" + htmlId -> "text/html" + else -> "" + } + suggestFormattingSyntax = choose + viewModel.formattingSyntax.value = choose + true + } + menu.show() + + return true + } + + private fun enableMarkdownWYSIWYGButtons(visible: Boolean) { + val visibility = if(visible) View.VISIBLE else View.GONE + codeButton.visibility = visibility + linkButton.visibility = visibility + strikethroughButton.visibility = visibility + italicButton.visibility = visibility + boldButton.visibility = visibility + } + + 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 + + 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) + } else { + var wasWord: Boolean + var isWord = end < editorText.length && !Character.isWhitespace(editorText[end]) + var newEnd = end + + // Iterate the selection backward so we don't have to juggle indices on insertion + var index = end - 1 + while (index >= start - 1 && index >= 0) { + wasWord = isWord + isWord = !Character.isWhitespace(editorText[index]) + if (wasWord && !isWord) { + // We've reached the beginning of a word, perform insert + editorText.insert(index + 1, text) + newEnd += text.length + } + --index + } + + if (start == 0 && isWord) { + // Special case when the selection includes the start of the text + editorText.insert(0, text) + newEnd += text.length + } + + // Keep the same text (including insertions) selected + composeEditField.setSelection(start, newEnd) + } + } + + + private fun atButtonClicked() { + prependSelectedWordsWith("@") + } + + private fun hashButtonClicked() { + prependSelectedWordsWith("#") + } + + private fun codeButtonClicked() { + when(viewModel.formattingSyntax.value!!) { + "text/markdown" -> MarkdownEdit.addCode(composeEditField) + "text/bbcode" -> BBCodeEdit.addCode(composeEditField) + "text/html" -> HTMLEdit.addCode(composeEditField) + } + } + + private fun linkButtonClicked() { + when(viewModel.formattingSyntax.value!!) { + "text/markdown" -> MarkdownEdit.addLink(composeEditField) + "text/bbcode" -> BBCodeEdit.addLink(composeEditField) + "text/html" -> HTMLEdit.addLink(composeEditField) + } + } + + private fun strikethroughButtonClicked() { + when(viewModel.formattingSyntax.value!!) { + "text/markdown" -> MarkdownEdit.addStrikeThrough(composeEditField) + "text/bbcode" -> BBCodeEdit.addStrikeThrough(composeEditField) + "text/html" -> HTMLEdit.addStrikeThrough(composeEditField) + } + } + + private fun italicButtonClicked() { + when(viewModel.formattingSyntax.value!!) { + "text/markdown" -> MarkdownEdit.addItalic(composeEditField) + "text/bbcode" -> BBCodeEdit.addItalic(composeEditField) + "text/html" -> HTMLEdit.addItalic(composeEditField) + } + } + + private fun boldButtonClicked() { + when(viewModel.formattingSyntax.value!!) { + "text/markdown" -> MarkdownEdit.addBold(composeEditField) + "text/bbcode" -> BBCodeEdit.addBold(composeEditField) + "text/html" -> HTMLEdit.addBold(composeEditField) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri) + super.onSaveInstanceState(outState) + } + + private fun displayTransientError(@StringRes stringId: Int) { + val bar = Snackbar.make(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() + } + + private fun toggleHideMedia() { + this.viewModel.toggleMarkSensitive() + } + + private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { + if (viewModel.media.value.isNullOrEmpty()) { + composeHideMediaButton.hide() + } else { + composeHideMediaButton.show() + @ColorInt val color = if (contentWarningShown) { + composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + composeHideMediaButton.isClickable = false + ContextCompat.getColor(this, R.color.transparent_tusky_blue) + + } else { + composeHideMediaButton.isClickable = true + if (markMediaSensitive) { + composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + ContextCompat.getColor(this, R.color.tusky_blue) + } else { + composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) + ThemeUtils.getColor(this, android.R.attr.textColorTertiary) + } + } + composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + } + } + + private fun updateScheduleButton() { + @ColorInt val color = if (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) + } + + private fun enableButtons(enable: Boolean) { + composeAddMediaButton.isClickable = enable + composeToggleVisibilityButton.isClickable = enable + composeEmojiButton.isClickable = enable + composeHideMediaButton.isClickable = enable + composeScheduleButton.isClickable = enable + composeFormattingSyntax.isClickable = enable + composeTootButton.isEnabled = enable + composePreviewButton.isEnabled = enable + composeStickerButton.isEnabled = enable + } + + private fun setStatusVisibility(visibility: Status.Visibility) { + composeOptionsBottomSheet.setStatusVisibility(visibility) + composeTootButton.setStatusVisibility(visibility) + + val iconRes = when (visibility) { + Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp + Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp + Status.Visibility.DIRECT -> R.drawable.ic_email_24dp + Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp + else -> R.drawable.ic_lock_open_24dp + } + composeToggleVisibilityButton.setImageResource(iconRes) + } + + private fun showComposeOptions() { + if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_HIDDEN || composeOptionsBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun onScheduleClick() { + if (viewModel.scheduledAt.value == null) { + composeScheduleView.openPickDateDialog() + } else { + showScheduleView() + } + } + + private fun showScheduleView() { + if (scheduleBehavior.state == BottomSheetBehavior.STATE_HIDDEN || scheduleBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + scheduleBehavior.state = BottomSheetBehavior.STATE_EXPANDED + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun showEmojis() { + 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() + } else { + if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + } + } + + private fun openPickDialog() { + if (addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun onMediaPick() { + addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + //Wait until bottom sheet is not collapsed and show next screen after + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.removeBottomSheetCallback(this) + if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this@ComposeActivity, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + } else { + initiateMediaPicking() + } + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + } + ) + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun openPollDialog() { + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + val instanceParams = viewModel.instanceParams.value!! + showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions, + instanceParams.pollMaxLength, viewModel::updatePoll) + } + + private fun setupPollView() { + val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + + val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + layoutParams.setMargins(margin, margin, margin, marginBottom) + pollPreview.layoutParams = layoutParams + + pollPreview.setOnClickListener { + val popup = PopupMenu(this, pollPreview) + val editId = 1 + val removeId = 2 + popup.menu.add(0, editId, 0, R.string.edit_poll) + popup.menu.add(0, removeId, 0, R.string.action_remove) + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + editId -> openPollDialog() + removeId -> removePoll() + } + true + } + popup.show() + } + } + + private fun removePoll() { + viewModel.poll.value = null + pollPreview.hide() + } + + override fun onVisibilityChanged(visibility: Status.Visibility) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.statusVisibility.value = visibility + } + + @VisibleForTesting + fun calculateTextLength(): Int { + var offset = 0 + val urlSpans = composeEditField.urls + if (urlSpans != null) { + for (span in urlSpans) { + offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) + } + } + var length = composeEditField.length() - offset + if (viewModel.showContentWarning.value!!) { + length += composeContentWarningField.length() + } + return length + } + + private fun updateVisibleCharactersLeft() { + val remainingLength = maximumTootCharacters - calculateTextLength(); + 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) + } + + private fun onContentWarningChanged() { + val showWarning = composeContentWarningBar.isGone + viewModel.contentWarningChanged(showWarning) + updateVisibleCharactersLeft() + } + + private fun verifyScheduledTime(): Boolean { + return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value)) + } + + private fun onSendClicked(preview: Boolean) { + if(preview && previewBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + if (verifyScheduledTime()) { + sendStatus(preview) + } else { + showScheduleView() + } + } + + private fun onStatusPreviewReady(status: Status) { + enableButtons(true) + previewView.setupWithStatus(status) + previewBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + /** This is for the fancy keyboards which can insert images and stuff. */ + override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { + // Verify the returned content's type is of the correct MIME type + val supported = inputContentInfo.description.hasMimeType("image/*") + + if (supported) { + val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + if (lacksPermission) { + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) + return false + } + } + pickMedia(inputContentInfo.contentUri, inputContentInfo) + return true + } + + return false + } + + private fun sendStatus(preview: Boolean) { + enableButtons(false) + val contentText = composeEditField.text.toString() + var spoilerText = "" + if (viewModel.showContentWarning.value!!) { + spoilerText = composeContentWarningField.text.toString() + } + val characterCount = calculateTextLength() + if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) { + composeEditField.error = getString(R.string.error_empty) + enableButtons(true) + } else if (characterCount <= maximumTootCharacters) { + if (viewModel.media.value!!.isNotEmpty()) { + finishingUploadDialog = ProgressDialog.show( + this, getString(R.string.dialog_title_finishing_media_upload), + getString(R.string.dialog_message_uploading_media), true, true) + } + + viewModel.sendStatus(contentText, spoilerText, preview).observeOnce(this) { + finishingUploadDialog?.dismiss() + if(!preview) + deleteDraftAndFinish() + } + + } else { + composeEditField.error = getString(R.string.error_compose_character_limit) + enableButtons(true) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initiateMediaPicking() + } else { + val bar = Snackbar.make(activityCompose, R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT).apply { + + } + bar.setAction(R.string.action_retry) { onMediaPick() } + //necessary so snackbar is shown over everything + bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.show() + } + } + } + + private fun initiateCameraApp() { + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + + // We don't need to ask for permission in this case, because the used calls require + // android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was + // way before permission dialogues have been introduced. + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + if (intent.resolveActivity(packageManager) != null) { + val photoFile: File = try { + createNewImageFile(this) + } catch (ex: IOException) { + displayTransientError(R.string.error_media_upload_opening) + return + } + + // Continue only if the File was successfully created + photoUploadUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID + ".fileprovider", + photoFile) + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri) + startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT) + } + } + + private fun initiateMediaPicking() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + + if(!viewModel.hasNoAttachmentLimits) { + val mimeTypes = arrayOf("image/*", "video/*", "audio/*") + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + intent.type = "*/*" + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + startActivityForResult(intent, MEDIA_PICK_RESULT) + } + + private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { + button.isEnabled = clickable + ThemeUtils.setDrawableTint(this, button.drawable, + if (colorActive) android.R.attr.textColorTertiary + else R.attr.textColorDisabled) + } + + private fun enablePollButton(enable: Boolean) { + 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) + } + + private fun removeMediaFromQueue(item: QueuedMedia) { + viewModel.removeMediaFromQueue(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { + if (intent.data != null) { + // Single media, upload it and done. + pickMedia(intent.data!!) + } else if (intent.clipData != null) { + val clipData = intent.clipData!! + 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() + } else { + // if not grater then 4, upload all multiple media. + for (i in 0 until count) { + val imageUri = clipData.getItemAt(i).getUri() + pickMedia(imageUri) + } + } + } + } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { + pickMedia(photoUploadUri!!) + } + } + + private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) { + withLifecycleContext { + viewModel.pickMedia(uri, filename ?: uri.toFileName(contentResolver)).observe { exceptionOrItem -> + + contentInfoCompat?.releasePermission() + + exceptionOrItem.asLeftOrNull()?.let { + val errorId = when (it) { + is VideoSizeException -> { + R.string.error_video_upload_size + } + is MediaSizeException -> { + R.string.error_media_upload_size + } + is AudioSizeException -> { + R.string.error_audio_upload_size + } + is VideoOrImageException -> { + R.string.error_media_upload_image_or_video + } + else -> { + Log.d(TAG, "That file could not be opened", it) + R.string.error_media_upload_opening + } + } + displayTransientError(errorId) + } + + } + } + } + + private fun showContentWarning(show: Boolean) { + TransitionManager.beginDelayedTransition(composeContentWarningBar.parent as ViewGroup) + @ColorInt val color = if (show) { + composeContentWarningBar.show() + composeContentWarningField.setSelection(composeContentWarningField.text.length) + composeContentWarningField.requestFocus() + ContextCompat.getColor(this, R.color.tusky_blue) + } else { + composeContentWarningBar.hide() + composeEditField.requestFocus() + ThemeUtils.getColor(this, android.R.attr.textColorTertiary) + } + composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + handleCloseButton() + return true + } + + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + // Acting like a teen: deliberately ignoring parent. + if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + stickerBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + previewBehavior.state == BottomSheetBehavior.STATE_HIDDEN) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + return + } + + handleCloseButton() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + Log.d(TAG, event.toString()) + if (event.action == KeyEvent.ACTION_DOWN) { + if (event.isCtrlPressed) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + // send toot by pressing CTRL + ENTER + this.onSendClicked(false) + return true + } + } + + if (keyCode == KeyEvent.KEYCODE_BACK) { + onBackPressed() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + private fun handleCloseButton() { + val contentText = composeEditField.text.toString() + val contentWarning = composeContentWarningField.text.toString() + if (viewModel.didChange(contentText, contentWarning)) { + AlertDialog.Builder(this) + .setMessage(R.string.compose_save_draft) + .setPositiveButton(R.string.action_save) { _, _ -> + saveDraftAndFinish(contentText, contentWarning) + } + .setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() } + .show() + } else { + finishWithoutSlideOutAnimation() + } + } + + private fun deleteDraftAndFinish() { + viewModel.deleteDraft() + finishWithoutSlideOutAnimation() + } + + private fun saveDraftAndFinish(contentText: String, contentWarning: String) { + viewModel.saveDraft(contentText, contentWarning) + finishWithoutSlideOutAnimation() + } + + override fun search(token: String): List { + return viewModel.searchAutocompleteSuggestions(token) + } + + override fun onEmojiSelected(shortcode: String) { + replaceTextAtCaret(":$shortcode: ") + } + + private fun setEmojiList(emojiList: List?) { + if (emojiList != null) { + emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) + enableButton(composeEmojiButton, true, emojiList.isNotEmpty()) + } + } + + private fun showStickers() { + if (stickerBehavior.state == BottomSheetBehavior.STATE_HIDDEN || stickerBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + stickerBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + override fun onEmojiSelected(id: String, shortcode: String) { + // pickMedia(Uri.parse(shortcode)) + + Glide.with(this).asFile().load(shortcode).into( object : CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) { + displayTransientError(R.string.error_sticker_fetch) + } + + override fun onResourceReady(resource: File, transition: Transition?) { + val cut = shortcode.lastIndexOf('/') + val filename = if(cut != -1) shortcode.substring(cut + 1) else "unknown.png" + pickMedia(resource.toUri(), null, filename) + } + }) + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + data class QueuedMedia( + val localId: Long, + val uri: Uri, + val type: Int, + val mediaSize: Long, + val originalFileName: String, + val noChanges: Boolean = false, + val anonymizeFileName: Boolean = false, + val uploadPercent: Int = 0, + val id: String? = null, + val description: String? = null + ) { + companion object Type { + public const val IMAGE: Int = 0 + public const val VIDEO: Int = 1 + public const val AUDIO: Int = 2 + public const val UNKNOWN: Int = 3 + } + } + + override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { + composeScheduleView.onTimeSet(hourOfDay, minute) + viewModel.updateScheduledAt(composeScheduleView.time) + if (verifyScheduledTime()) { + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + showScheduleView() + } + } + + private fun resetSchedule() { + viewModel.updateScheduledAt(null) + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + @Parcelize + data class ComposeOptions( + // Let's keep fields var until all consumers are Kotlin + var scheduledTootId: String? = null, + var savedTootUid: Int? = null, + var draftId: Int? = null, + var tootText: String? = null, + var mediaUrls: List? = null, + var mediaDescriptions: List? = null, + var mentionedUsernames: Set? = null, + var inReplyToId: String? = null, + var replyVisibility: Status.Visibility? = null, + var visibility: Status.Visibility? = null, + var contentWarning: String? = null, + var replyingStatusAuthor: String? = null, + var replyingStatusContent: String? = null, + var mediaAttachments: List? = null, + var draftAttachments: List? = null, + var scheduledAt: String? = null, + var sensitive: Boolean? = null, + var poll: NewPoll? = null, + var formattingSyntax: String? = null, + var modifiedInitialState: Boolean? = null + ) : Parcelable + + companion object { + private const val TAG = "ComposeActivity" // logging tag + private const val MEDIA_PICK_RESULT = 1 + private const val MEDIA_TAKE_PHOTO_RESULT = 2 + private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 + + internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" + private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" + + // Mastodon only counts URLs as this long in terms of status character limits + @VisibleForTesting + const val MAXIMUM_URL_LENGTH = 23 + + @JvmStatic + fun startIntent(context: Context, options: ComposeOptions): Intent { + return Intent(context, ComposeActivity::class.java).apply { + putExtra(COMPOSE_OPTIONS_EXTRA, options) + } + } + + fun canHandleMimeType(mimeType: String?): Boolean { + return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java new file mode 100644 index 0000000..09d7068 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java @@ -0,0 +1,320 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose; + +import android.content.Context; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.HashTag; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Created by charlag on 12/11/17. + */ + +public class ComposeAutoCompleteAdapter extends BaseAdapter + implements Filterable { + private static final int ACCOUNT_VIEW_TYPE = 1; + private static final int HASHTAG_VIEW_TYPE = 2; + private static final int EMOJI_VIEW_TYPE = 3; + private static final int SEPARATOR_VIEW_TYPE = 0; + + private final ArrayList resultList; + private final AutocompletionProvider autocompletionProvider; + + public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { + super(); + resultList = new ArrayList<>(); + this.autocompletionProvider = autocompletionProvider; + } + + @Override + public int getCount() { + return resultList.size(); + } + + @Override + public AutocompleteResult getItem(int index) { + return resultList.get(index); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + @NonNull + public Filter getFilter() { + return new Filter() { + @Override + public CharSequence convertResultToString(Object resultValue) { + if (resultValue instanceof AccountResult) { + return formatUsername(((AccountResult) resultValue)); + } else if (resultValue instanceof HashtagResult) { + return formatHashtag((HashtagResult) resultValue); + } else if (resultValue instanceof EmojiResult) { + return formatEmoji((EmojiResult) resultValue); + } else { + return ""; + } + } + + // This method is invoked in a worker thread. + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults filterResults = new FilterResults(); + if (constraint != null) { + List results = + autocompletionProvider.search(constraint.toString()); + filterResults.values = results; + filterResults.count = results.size(); + } + return filterResults; + } + + @SuppressWarnings("unchecked") + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + if (results != null && results.count > 0) { + resultList.clear(); + resultList.addAll((List) results.values); + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + }; + } + + @Override + @NonNull + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = convertView; + final Context context = parent.getContext(); + + switch (getItemViewType(position)) { + case ACCOUNT_VIEW_TYPE: + AccountViewHolder accountViewHolder; + if (convertView == null) { + view = ((LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_account, parent, false); + } + if (view.getTag() == null) { + view.setTag(new AccountViewHolder(view)); + } + accountViewHolder = (AccountViewHolder) view.getTag(); + + AccountResult accountResult = ((AccountResult) getItem(position)); + if (accountResult != null) { + Account account = accountResult.account; + String formattedUsername = context.getString( + R.string.status_username_format, + account.getUsername() + ); + accountViewHolder.username.setText(formattedUsername); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), + account.getEmojis(), accountViewHolder.displayName); + accountViewHolder.displayName.setText(emojifiedName); + + int avatarRadius = accountViewHolder.avatar.getContext().getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_42dp); + + boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext()) + .getBoolean("animateGifAvatars", false); + + ImageLoadingHelper.loadAvatar( + account.getAvatar(), + accountViewHolder.avatar, + avatarRadius, + animateAvatar + ); + } + break; + + case HASHTAG_VIEW_TYPE: + if (convertView == null) { + view = ((LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_hashtag, parent, false); + } + + HashtagResult result = (HashtagResult) getItem(position); + if (result != null) { + ((TextView) view).setText(formatHashtag(result)); + } + break; + + case EMOJI_VIEW_TYPE: + EmojiViewHolder emojiViewHolder; + if (convertView == null) { + view = ((LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_emoji, parent, false); + } + if (view.getTag() == null) { + view.setTag(new EmojiViewHolder(view)); + } + emojiViewHolder = (EmojiViewHolder) view.getTag(); + + EmojiResult emojiResult = ((EmojiResult) getItem(position)); + if (emojiResult != null) { + Emoji emoji = emojiResult.emoji; + String formattedShortcode = context.getString( + R.string.emoji_shortcode_format, + emoji.getShortcode() + ); + emojiViewHolder.shortcode.setText(formattedShortcode); + Glide.with(emojiViewHolder.preview) + .load(emoji.getUrl()) + .into(emojiViewHolder.preview); + } + break; + + case SEPARATOR_VIEW_TYPE: + if (convertView == null) { + view = ((LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_divider, parent, false); + } + break; + default: + throw new AssertionError("unknown view type"); + } + + return view; + } + + private static String formatUsername(AccountResult result) { + return String.format("@%s", result.account.getUsername()); + } + + private static String formatHashtag(HashtagResult result) { + return String.format("#%s", result.hashtag); + } + + private static String formatEmoji(EmojiResult result) { + return String.format(":%s:", result.emoji.getShortcode()); + } + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + AutocompleteResult item = getItem(position); + + if (item instanceof AccountResult) { + return ACCOUNT_VIEW_TYPE; + } else if (item instanceof HashtagResult) { + return HASHTAG_VIEW_TYPE; + } else if (item instanceof EmojiResult) { + return EMOJI_VIEW_TYPE; + } else { + return SEPARATOR_VIEW_TYPE; + } + } + + @Override + public boolean areAllItemsEnabled() { + // there may be separators + return false; + } + + @Override + public boolean isEnabled(int position) { + return !(getItem(position) instanceof ResultSeparator); + } + + public abstract static class AutocompleteResult { + AutocompleteResult() { + } + } + + public final static class AccountResult extends AutocompleteResult { + public final Account account; + + public AccountResult(Account account) { + this.account = account; + } + } + + public final static class HashtagResult extends AutocompleteResult { + private final String hashtag; + + public HashtagResult(HashTag hashtag) { + this.hashtag = hashtag.getName(); + } + } + + public final static class EmojiResult extends AutocompleteResult { + private final Emoji emoji; + + public EmojiResult(Emoji emoji) { + this.emoji = emoji; + } + } + + public final static class ResultSeparator extends AutocompleteResult {} + + public interface AutocompletionProvider { + List search(String mention); + } + + private class AccountViewHolder { + final TextView username; + final TextView displayName; + final ImageView avatar; + + private AccountViewHolder(View view) { + username = view.findViewById(R.id.username); + displayName = view.findViewById(R.id.display_name); + avatar = view.findViewById(R.id.avatar); + } + } + + private class EmojiViewHolder { + final TextView shortcode; + final ImageView preview; + + private EmojiViewHolder(View view) { + shortcode = view.findViewById(R.id.shortcode); + preview = view.findViewById(R.id.preview); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt new file mode 100644 index 0000000..bfcfa93 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -0,0 +1,295 @@ +/* Copyright 2019 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.compose + +import android.net.Uri +import android.util.Log +import androidx.core.net.toUri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.keylesspalace.tusky.components.common.CommonComposeViewModel +import com.keylesspalace.tusky.components.common.MediaUploader +import com.keylesspalace.tusky.components.common.mutableLiveData +import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.service.ServiceClient +import com.keylesspalace.tusky.service.TootToSend +import com.keylesspalace.tusky.util.* +import io.reactivex.Observable.just +import java.util.* +import javax.inject.Inject + +class ComposeViewModel @Inject constructor( + private val api: MastodonApi, + private val accountManager: AccountManager, + private val mediaUploader: MediaUploader, + private val serviceClient: ServiceClient, + private val draftHelper: DraftHelper, + private val saveTootHelper: SaveTootHelper, + private val db: AppDatabase +) : CommonComposeViewModel(api, accountManager, mediaUploader, db) { + + private var replyingStatusAuthor: String? = null + private var replyingStatusContent: String? = null + internal var startingText: String? = null + private var savedTootUid: Int = 0 + private var draftId: Int = 0 + private var scheduledTootId: String? = null + private var startingContentWarning: String = "" + private var inReplyToId: String? = null + private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN + private var contentWarningStateChanged: Boolean = false + private var modifiedInitialState: Boolean = false + + val markMediaAsSensitive = + mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + + val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) + val showContentWarning = mutableLiveData(false) + val setupComplete = mutableLiveData(false) + val poll: MutableLiveData = mutableLiveData(null) + val scheduledAt: MutableLiveData = mutableLiveData(null) + val formattingSyntax: MutableLiveData = mutableLiveData("") + + private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() + + fun toggleMarkSensitive() { + this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true + } + + fun didChange(content: String?, contentWarning: String?): Boolean { + + val textChanged = !(content.isNullOrEmpty() + || startingText?.startsWith(content.toString()) ?: false) + + val contentWarningChanged = showContentWarning.value!! + && !contentWarning.isNullOrEmpty() + && !startingContentWarning.startsWith(contentWarning.toString()) + val mediaChanged = !media.value.isNullOrEmpty() + val pollChanged = poll.value != null + + return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged + } + + fun contentWarningChanged(value: Boolean) { + showContentWarning.value = value + contentWarningStateChanged = true + } + + fun deleteDraft() { + if (savedTootUid != 0) { + saveTootHelper.deleteDraft(savedTootUid) + } + if (draftId != 0) { + draftHelper.deleteDraftAndAttachments(draftId) + .subscribe() + } + } + + fun saveDraft(content: String, contentWarning: String) { + + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + media.value?.forEach { item -> + mediaUris.add(item.uri.toString()) + mediaDescriptions.add(item.description) + } + draftHelper.saveDraft( + draftId = draftId, + accountId = accountManager.activeAccount?.id!!, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = markMediaAsSensitive.value!!, + visibility = statusVisibility.value!!, + mediaUris = mediaUris, + mediaDescriptions = mediaDescriptions, + poll = poll.value, + formattingSyntax = formattingSyntax.value!!, + failedToSend = false + ).subscribe() + } + + /** + * Send status to the server. + * Uses current state plus provided arguments. + * @return LiveData which will signal once the screen can be closed or null if there are errors + */ + fun sendStatus( + content: String, + spoilerText: String, + preview: Boolean + ): LiveData { + + val deletionObservable = if (isEditingScheduledToot) { + api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } + } else { + just(Unit) + }.toLiveData() + + val sendObservable = media + .filter { items -> items.all { it.uploadPercent == -1 } } + .map { + val mediaIds = ArrayList() + val mediaUris = ArrayList() + val mediaDescriptions = ArrayList() + for (item in media.value!!) { + mediaIds.add(item.id!!) + mediaUris.add(item.uri) + mediaDescriptions.add(item.description ?: "") + } + + val tootToSend = TootToSend( + text = content, + warningText = spoilerText, + visibility = statusVisibility.value!!.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + mediaIds = mediaIds, + mediaUris = mediaUris.map { it.toString() }, + mediaDescriptions = mediaDescriptions, + scheduledAt = scheduledAt.value, + inReplyToId = inReplyToId, + poll = poll.value, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + formattingSyntax = formattingSyntax.value!!, + preview = preview, + accountId = accountManager.activeAccount!!.id, + savedTootUid = savedTootUid, + draftId = draftId, + idempotencyKey = randomAlphanumericString(16), + retries = 0 + ) + + serviceClient.sendToot(tootToSend) + } + + return combineLiveData(deletionObservable, sendObservable) { _, _ -> } + } + + fun setup(composeOptions: ComposeActivity.ComposeOptions?) { + + if (setupComplete.value == true) { + return + } + + val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy + + val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN + startingVisibility = Status.Visibility.byNum( + preferredVisibility.num.coerceAtLeast(replyVisibility.num)) + + inReplyToId = composeOptions?.inReplyToId + + modifiedInitialState = composeOptions?.modifiedInitialState == true + + val contentWarning = composeOptions?.contentWarning + if (contentWarning != null) { + startingContentWarning = contentWarning + } + if (!contentWarningStateChanged) { + showContentWarning.value = !contentWarning.isNullOrBlank() + } + + // recreate media list + val loadedDraftMediaUris = composeOptions?.mediaUrls + val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions + val draftAttachments = composeOptions?.draftAttachments + if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { + // when coming from SavedTootActivity + loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) + .forEach { (uri, description) -> + pickMedia(uri.toUri(), null).observeForever { errorOrItem -> + if (errorOrItem.isRight() && description != null) { + updateDescription(errorOrItem.asRight().localId, description) + } + } + } + } else if (draftAttachments != null) { + // when coming from DraftActivity + draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } + } else composeOptions?.mediaAttachments?.forEach { a -> + // when coming from redraft or ScheduledTootActivity + val mediaType = when (a.type) { + Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO + Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE + Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO + } + addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) + } + + savedTootUid = composeOptions?.savedTootUid ?: 0 + draftId = composeOptions?.draftId ?: 0 + scheduledTootId = composeOptions?.scheduledTootId + startingText = composeOptions?.tootText + + val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN + if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { + startingVisibility = tootVisibility + } + statusVisibility.value = startingVisibility + val mentionedUsernames = composeOptions?.mentionedUsernames + if (mentionedUsernames != null) { + val builder = StringBuilder() + for (name in mentionedUsernames) { + builder.append('@') + builder.append(name) + builder.append(' ') + } + startingText = builder.toString() + } + + scheduledAt.value = composeOptions?.scheduledAt + + composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } + + val poll = composeOptions?.poll + if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) { + this.poll.value = poll + } + replyingStatusContent = composeOptions?.replyingStatusContent + replyingStatusAuthor = composeOptions?.replyingStatusAuthor + + formattingSyntax.value = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax + } + + fun updatePoll(newPoll: NewPoll) { + poll.value = newPoll + } + + fun updateScheduledAt(newScheduledAt: String?) { + scheduledAt.value = newScheduledAt + } + + override fun onCleared() { + for (uploadDisposable in mediaToDisposable.values) { + uploadDisposable.dispose() + } + super.onCleared() + } + + private companion object { + const val TAG = "ComposeViewModel" + } +} 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 new file mode 100644 index 0000000..f49a05e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -0,0 +1,159 @@ +/* Copyright 2019 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.compose + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import android.view.Gravity +import android.text.TextUtils +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.view.ProgressTextView +import com.keylesspalace.tusky.components.compose.view.ProgressImageView + +class MediaPreviewAdapter( + context: Context, + private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, + private val onRemove: (ComposeActivity.QueuedMedia) -> Unit +) : RecyclerView.Adapter() { + + fun submitList(list: List) { + this.differ.submitList(list) + } + + private fun onMediaClick(position: Int, view: View) { + val item = differ.currentList[position] + val popup = PopupMenu(view.context, view) + val addCaptionId = 1 + val removeId = 2 + popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) + popup.menu.add(0, removeId, 0, R.string.action_remove) + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + addCaptionId -> onAddCaption(item) + removeId -> onRemove(item) + } + true + } + popup.show() + } + + private val thumbnailViewSize = + context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + + override fun getItemCount(): Int = differ.currentList.size + + override fun getItemViewType(position: Int): Int { + val item = differ.currentList[position] + return item.type + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when(viewType) { + ComposeActivity.QueuedMedia.Type.UNKNOWN -> { + return TextViewHolder(ProgressTextView(parent.context)) + } + else -> { + return PreviewViewHolder(ProgressImageView(parent.context)) + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = differ.currentList[position] + + when(item.type) { + ComposeActivity.QueuedMedia.Type.UNKNOWN -> { + (holder as TextViewHolder).view.setText(item.originalFileName) + holder.view.setChecked(!item.description.isNullOrEmpty()) + holder.view.setProgress(item.uploadPercent) + } + ComposeActivity.QueuedMedia.Type.AUDIO -> { + (holder as PreviewViewHolder).view.setChecked(!item.description.isNullOrEmpty()) + holder.view.setProgress(item.uploadPercent) + holder.view.setImageResource(R.drawable.ic_music_box_preview_24dp) + } + else -> { + (holder as PreviewViewHolder).view.setChecked(!item.description.isNullOrEmpty()) + holder.view.setProgress(item.uploadPercent) + + Glide.with(holder.itemView.context) + .load(item.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.view) + } + } + } + + private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + return oldItem.localId == newItem.localId + } + + override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + return oldItem == newItem + } + }) + + inner class TextViewHolder(val view: ProgressTextView) + : RecyclerView.ViewHolder(view) { + init { + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + view.layoutParams = layoutParams + view.gravity = Gravity.CENTER + view.setHorizontallyScrolling(true) + view.ellipsize = TextUtils.TruncateAt.MARQUEE + view.marqueeRepeatLimit = -1 + view.setSingleLine() + view.setSelected(true) + view.textSize = 16.0f + view.setOnClickListener { + onMediaClick(adapterPosition, view) + } + } + } + + inner class PreviewViewHolder(val view: ProgressImageView) + : RecyclerView.ViewHolder(view) { + init { + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + view.layoutParams = layoutParams + view.scaleType = ImageView.ScaleType.CENTER_CROP + view.setOnClickListener { + onMediaClick(adapterPosition, view) + } + } + } +} 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 new file mode 100644 index 0000000..d0f98ba --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -0,0 +1,101 @@ +/* Copyright 2019 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 . */ + +@file:JvmName("AddPollDialog") + +package com.keylesspalace.tusky.components.compose.dialog + +import android.content.Context +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.entity.NewPoll +import kotlinx.android.synthetic.main.dialog_add_poll.view.* + +fun showAddPollDialog( + context: Context, + poll: NewPoll?, + maxOptionCount: Int, + maxOptionLength: Int, + onUpdatePoll: (NewPoll) -> Unit +) { + + val view = LayoutInflater.from(context).inflate(R.layout.dialog_add_poll, null) + + val dialog = AlertDialog.Builder(context) + .setIcon(R.drawable.ic_poll_24dp) + .setTitle(R.string.create_poll_title) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .create() + + val adapter = AddPollOptionsAdapter( + options = poll?.options?.toMutableList() ?: mutableListOf("", ""), + maxOptionLength = maxOptionLength, + onOptionRemoved = { valid -> + view.addChoiceButton.isEnabled = true + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + }, + onOptionChanged = { valid -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + } + ) + + view.pollChoices.adapter = adapter + + view.addChoiceButton.setOnClickListener { + if (adapter.itemCount < maxOptionCount) { + adapter.addChoice() + } + if (adapter.itemCount >= maxOptionCount) { + it.isEnabled = false + } + } + + val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast { + it <= poll?.expiresIn ?: 0 + } + + view.pollDurationSpinner.setSelection(pollDurationId) + + view.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false + + dialog.setOnShowListener { + val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + button.setOnClickListener { + val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition + + val pollDuration = context.resources + .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] + + onUpdatePoll(NewPoll( + options = adapter.pollOptions, + expiresIn = pollDuration, + multiple = view.multipleChoicesCheckBox.isChecked + )) + + dialog.dismiss() + } + } + + dialog.show() + + // 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/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt new file mode 100644 index 0000000..9399858 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -0,0 +1,118 @@ +/* Copyright 2019 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.compose.dialog + +import android.app.Activity +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.InputFilter +import android.text.InputType +import android.util.DisplayMetrics +import android.view.WindowManager +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import at.connyduck.sparkbutton.helpers.Utils +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.github.piasy.biv.loader.glide.GlideCustomImageLoader +import com.github.piasy.biv.view.BigImageView +import com.github.piasy.biv.loader.ImageLoader +import com.github.piasy.biv.view.GlideImageViewFactory +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.util.withLifecycleContext +import java.io.File + +// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 +private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 + +fun T.makeCaptionDialog(existingDescription: String?, + previewUri: Uri, + onUpdateDescription: (String) -> LiveData +) where T : Activity, T : LifecycleOwner { + val dialogLayout = LinearLayout(this) + val padding = Utils.dpToPx(this, 8) + dialogLayout.setPadding(padding, padding, padding, padding) + + dialogLayout.orientation = LinearLayout.VERTICAL + val imageView = BigImageView(this) + imageView.setImageViewFactory(GlideImageViewFactory()) + imageView.setImageLoaderCallback(object : ImageLoader.Callback { + override fun onSuccess(image: File?) { + imageView.ssiv?.let { it.maxScale = 6f } + } + override fun onFail(error: Exception?) {} + override fun onStart() {} + override fun onCacheHit(imageType: Int, image: File?) {} + override fun onCacheMiss(imageType: Int, image: File?) {} + override fun onFinish() {} + override fun onProgress(progress: Int) {} + }) + imageView.showImage(previewUri) + + val displayMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(displayMetrics) + + val margin = Utils.dpToPx(this, 4) + dialogLayout.addView(imageView) + (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f + imageView.layoutParams.height = 0 + (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) + dialogLayout.addView(input) + (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) + input.setLines(2) + input.inputType = (InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_FLAG_MULTI_LINE + or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) + input.setText(existingDescription) + input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) + + val okListener = { dialog: DialogInterface, _: Int -> + onUpdateDescription(input.text.toString()) + withLifecycleContext { + onUpdateDescription(input.text.toString()) + .observe { success -> if (!success) showFailedCaptionMessage() } + + } + + dialog.dismiss() + } + + val dialog = AlertDialog.Builder(this) + .setView(dialogLayout) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, null) + .create() + + val window = dialog.window + window?.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + + dialog.show() +} + +private fun Activity.showFailedCaptionMessage() { + Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt new file mode 100644 index 0000000..8f80c76 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt @@ -0,0 +1,70 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.RadioGroup +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Status + +class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) { + + var listener: ComposeOptionsListener? = null + + init { + inflate(context, R.layout.view_compose_options, this) + + setOnCheckedChangeListener { _, checkedId -> + val visibility = when (checkedId) { + R.id.publicRadioButton -> + Status.Visibility.PUBLIC + R.id.unlistedRadioButton -> + Status.Visibility.UNLISTED + R.id.privateRadioButton -> + Status.Visibility.PRIVATE + R.id.directRadioButton -> + Status.Visibility.DIRECT + else -> + Status.Visibility.PUBLIC + } + listener?.onVisibilityChanged(visibility) + } + } + + fun setStatusVisibility(visibility: Status.Visibility) { + val selectedButton = when (visibility) { + Status.Visibility.PUBLIC -> + R.id.publicRadioButton + Status.Visibility.UNLISTED -> + R.id.unlistedRadioButton + Status.Visibility.PRIVATE -> + R.id.privateRadioButton + Status.Visibility.DIRECT -> + R.id.directRadioButton + else -> + R.id.directRadioButton + + } + + check(selectedButton) + } + +} + +interface ComposeOptionsListener { + fun onVisibilityChanged(visibility: Status.Visibility) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java new file mode 100644 index 0000000..a1a99a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java @@ -0,0 +1,228 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; + +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.fragment.TimePickerFragment; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class ComposeScheduleView extends ConstraintLayout { + + private DateFormat dateFormat; + private DateFormat timeFormat; + private SimpleDateFormat iso8601; + + private Button resetScheduleButton; + private TextView scheduledDateTimeView; + private TextView invalidScheduleWarningView; + + private Calendar scheduleDateTime; + public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting + + public ComposeScheduleView(Context context) { + super(context); + init(); + } + + public ComposeScheduleView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ComposeScheduleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + inflate(getContext(), R.layout.view_compose_schedule, this); + + dateFormat = SimpleDateFormat.getDateInstance(); + timeFormat = SimpleDateFormat.getTimeInstance(); + iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()); + iso8601.setTimeZone(TimeZone.getTimeZone("UTC")); + + resetScheduleButton = findViewById(R.id.resetScheduleButton); + scheduledDateTimeView = findViewById(R.id.scheduledDateTime); + invalidScheduleWarningView = findViewById(R.id.invalidScheduleWarning); + + scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog()); + invalidScheduleWarningView.setText(R.string.warning_scheduling_interval); + + scheduleDateTime = null; + + setScheduledDateTime(); + + setEditIcons(); + } + + private void setScheduledDateTime() { + if (scheduleDateTime == null) { + scheduledDateTimeView.setText(""); + invalidScheduleWarningView.setVisibility(GONE); + } else { + Date scheduled = scheduleDateTime.getTime(); + scheduledDateTimeView.setText(String.format("%s %s", + dateFormat.format(scheduled), + timeFormat.format(scheduled))); + verifyScheduledTime(scheduled); + } + } + + private void setEditIcons() { + Drawable icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_create_24dp); + if (icon == null) { + return; + } + + final int size = scheduledDateTimeView.getLineHeight(); + + icon.setBounds(0, 0, size, size); + + scheduledDateTimeView.setCompoundDrawables(null, null, icon, null); + } + + public void setResetOnClickListener(OnClickListener listener) { + resetScheduleButton.setOnClickListener(listener); + } + + public void resetSchedule() { + scheduleDateTime = null; + setScheduledDateTime(); + } + + public void openPickDateDialog() { + long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000; + CalendarConstraints calendarConstraints = new CalendarConstraints.Builder() + .setValidator( + DateValidatorPointForward.from(yesterday)) + .build(); + initializeSuggestedTime(); + MaterialDatePicker picker = MaterialDatePicker.Builder + .datePicker() + .setSelection(scheduleDateTime.getTimeInMillis()) + .setCalendarConstraints(calendarConstraints) + .build(); + picker.addOnPositiveButtonClickListener(this::onDateSet); + picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "date_picker"); + } + + private void openPickTimeDialog() { + TimePickerFragment picker = new TimePickerFragment(); + if (scheduleDateTime != null) { + Bundle args = new Bundle(); + args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); + args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); + picker.setArguments(args); + } + picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); + } + + public Date getDateTime(String scheduledAt) { + if (scheduledAt != null) { + try { + return iso8601.parse(scheduledAt); + } catch (ParseException e) { + } + } + return null; + } + + public void setDateTime(String scheduledAt) { + Date date; + try { + date = iso8601.parse(scheduledAt); + } catch (ParseException e) { + return; + } + initializeSuggestedTime(); + scheduleDateTime.setTime(date); + setScheduledDateTime(); + } + + public boolean verifyScheduledTime(@Nullable Date scheduledTime) { + boolean valid; + if (scheduledTime != null) { + Calendar minimumScheduledTime = getCalendar(); + minimumScheduledTime.add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS); + valid = scheduledTime.after(minimumScheduledTime.getTime()); + } else { + valid = true; + } + invalidScheduleWarningView.setVisibility(valid ? GONE : VISIBLE); + return valid; + } + + private void onDateSet(long selection) { + initializeSuggestedTime(); + Calendar newDate = getCalendar(); + // working around bug in DatePicker where date is UTC #1720 + // see https://github.com/material-components/material-components-android/issues/882 + newDate.setTimeZone(TimeZone.getTimeZone("UTC")); + newDate.setTimeInMillis(selection); + scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); + openPickTimeDialog(); + } + + public void onTimeSet(int hourOfDay, int minute) { + initializeSuggestedTime(); + scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); + scheduleDateTime.set(Calendar.MINUTE, minute); + setScheduledDateTime(); + } + + public String getTime() { + if (scheduleDateTime == null) { + return null; + } + return iso8601.format(scheduleDateTime.getTime()); + } + + @NonNull + public static Calendar getCalendar() { + return Calendar.getInstance(TimeZone.getDefault()); + } + + private void initializeSuggestedTime() { + if (scheduleDateTime == null) { + scheduleDateTime = getCalendar(); + scheduleDateTime.add(Calendar.MINUTE, 15); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt new file mode 100644 index 0000000..0a5e1c3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -0,0 +1,65 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import androidx.emoji.widget.EmojiEditTextHelper +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView +import android.text.InputType +import android.text.method.KeyListener +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection + +class EditTextTyped @JvmOverloads constructor(context: Context, + attributeSet: AttributeSet? = null) + : AppCompatMultiAutoCompleteTextView(context, attributeSet) { + + private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null + private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this) + + init { + //fix a bug with autocomplete and some keyboards + val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) + inputType = newInputType + super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)) + } + + override fun setKeyListener(input: KeyListener) { + super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input)) + } + + fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) { + onCommitContentListener = listener + } + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { + val connection = super.onCreateInputConnection(editorInfo) + return if (onCommitContentListener != null) { + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) + getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo, + onCommitContentListener!!), editorInfo)!! + } else { + connection + } + } + + private fun getEmojiEditTextHelper(): EmojiEditTextHelper { + return emojiEditTextHelper + } +} 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 new file mode 100644 index 0000000..63e627f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt @@ -0,0 +1,64 @@ +/* Copyright 2019 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.compose.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter +import com.keylesspalace.tusky.entity.NewPoll +import kotlinx.android.synthetic.main.view_poll_preview.view.* + +class PollPreviewView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : LinearLayout(context, attrs, defStyleAttr) { + + val adapter = PreviewPollOptionsAdapter() + + init { + inflate(context, R.layout.view_poll_preview, this) + + orientation = VERTICAL + + setBackgroundResource(R.drawable.card_frame) + + val padding = resources.getDimensionPixelSize(R.dimen.poll_preview_padding) + + setPadding(padding, padding, padding, padding) + + pollPreviewOptions.adapter = adapter + + } + + fun setPoll(poll: NewPoll){ + adapter.update(poll.options, poll.multiple) + + val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { + it <= poll.expiresIn + } + 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/components/compose/view/ProgressImageView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java new file mode 100644 index 0000000..0811703 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java @@ -0,0 +1,122 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatImageView; +import android.util.AttributeSet; + +import com.keylesspalace.tusky.R; +import at.connyduck.sparkbutton.helpers.Utils; + +public final class ProgressImageView extends AppCompatImageView { + + private int progress = -1; + private final RectF progressRect = new RectF(); + private final RectF biggerRect = new RectF(); + private final Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint markBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Drawable captionDrawable; + + public ProgressImageView(Context context) { + super(context); + init(); + } + + public ProgressImageView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ProgressImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + circlePaint.setColor(ContextCompat.getColor(getContext(), R.color.tusky_blue)); + circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4)); + circlePaint.setStyle(Paint.Style.STROKE); + + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + + markBgPaint.setStyle(Paint.Style.FILL); + markBgPaint.setColor(ContextCompat.getColor(getContext(), + R.color.tusky_grey_10)); + captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck); + } + + public void setProgress(int progress) { + this.progress = progress; + if (progress != -1) { + setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY); + } else { + clearColorFilter(); + } + invalidate(); + } + + public void setChecked(boolean checked) { + this.markBgPaint.setColor(ContextCompat.getColor(getContext(), + checked ? R.color.tusky_blue : R.color.tusky_grey_10)); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float angle = (progress / 100f) * 360 - 90; + float halfWidth = getWidth() / 2.0f; + float halfHeight = getHeight() / 2.0f; + progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); + biggerRect.set(progressRect); + int margin = 8; + biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin); + canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG); + if (progress != -1) { + canvas.drawOval(progressRect, circlePaint); + canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint); + } + canvas.restore(); + + int circleRadius = Utils.dpToPx(getContext(), 14); + int circleMargin = Utils.dpToPx(getContext(), 14); + + int circleY = getHeight() - circleMargin - circleRadius / 2; + int circleX = getWidth() - circleMargin - circleRadius / 2; + + canvas.drawCircle(circleX, circleY, circleRadius, markBgPaint); + + captionDrawable.setBounds(getWidth() - circleMargin - circleRadius, + getHeight() - circleMargin - circleRadius, + getWidth() - circleMargin, + getHeight() - circleMargin); + captionDrawable.setTint(Color.WHITE); + captionDrawable.draw(canvas); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java new file mode 100644 index 0000000..8078f6e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java @@ -0,0 +1,121 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatTextView; +import android.util.AttributeSet; + +import com.keylesspalace.tusky.R; +import at.connyduck.sparkbutton.helpers.Utils; + +public final class ProgressTextView extends TextView { + + private int progress = -1; + private final RectF progressRect = new RectF(); + private final RectF biggerRect = new RectF(); + private final Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint markBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Drawable captionDrawable; + + public ProgressTextView(Context context) { + super(context); + init(); + } + + public ProgressTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ProgressTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + circlePaint.setColor(ContextCompat.getColor(getContext(), R.color.tusky_blue)); + circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4)); + circlePaint.setStyle(Paint.Style.STROKE); + + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + + markBgPaint.setStyle(Paint.Style.FILL); + markBgPaint.setColor(ContextCompat.getColor(getContext(), + R.color.tusky_grey_10)); + captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck); + } + + public void setProgress(int progress) { + this.progress = progress; + invalidate(); + } + + public void setChecked(boolean checked) { + this.markBgPaint.setColor(ContextCompat.getColor(getContext(), + checked ? R.color.tusky_blue : R.color.tusky_grey_10)); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // https://stackoverflow.com/questions/25501185/ + canvas.translate(getScrollX(), 0); + + float angle = (progress / 100f) * 360 - 90; + float halfWidth = getWidth() / 2.0f; + float halfHeight = getHeight() / 2.0f; + progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); + biggerRect.set(progressRect); + int margin = 8; + biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin); + canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG); + if (progress != -1) { + canvas.drawOval(progressRect, circlePaint); + canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint); + } + canvas.restore(); + + int circleRadius = Utils.dpToPx(getContext(), 14); + int circleMargin = Utils.dpToPx(getContext(), 14); + + int circleY = getHeight() - circleMargin - circleRadius / 2; + int circleX = getWidth() - circleMargin - circleRadius / 2; + + canvas.drawCircle(circleX, circleY, circleRadius, markBgPaint); + + captionDrawable.setBounds(getWidth() - circleMargin - circleRadius, + getHeight() - circleMargin - circleRadius, + getWidth() - circleMargin, + getHeight() - circleMargin); + captionDrawable.setTint(Color.WHITE); + captionDrawable.draw(canvas); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt new file mode 100644 index 0000000..f7ba7ee --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt @@ -0,0 +1,75 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import com.google.android.material.button.MaterialButton +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Status +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 + +class TootButton +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button) + + init { + if(smallStyle) { + setIconResource(R.drawable.ic_send_24dp) + } else { + setText(R.string.action_send) + iconGravity = ICON_GRAVITY_TEXT_START + } + val padding = resources.getDimensionPixelSize(R.dimen.toot_button_horizontal_padding) + setPadding(padding, 0, padding, 0) + } + + fun setStatusVisibility(visibility: Status.Visibility) { + if(!smallStyle) { + + icon = when (visibility) { + Status.Visibility.PUBLIC -> { + setText(R.string.action_send_public) + null + } + Status.Visibility.UNLISTED -> { + setText(R.string.action_send) + null + } + Status.Visibility.PRIVATE, + Status.Visibility.DIRECT -> { + setText(R.string.action_send) + IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply { sizeDp = 18; colorInt = Color.WHITE } + } + else -> { + null + } + } + } + + } + +} + 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 new file mode 100644 index 0000000..6d6aee4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -0,0 +1,110 @@ +package com.keylesspalace.tusky.components.conversation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.AsyncPagedListDiffer +import androidx.paging.PagedList +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +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.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.NetworkState +import com.keylesspalace.tusky.util.StatusDisplayOptions + +class ConversationAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val listener: StatusActionListener, + private val topLoadedCallback: () -> Unit, + private val retryCallback: () -> Unit +) : RecyclerView.Adapter() { + + private var networkState: NetworkState? = null + + private val differ: AsyncPagedListDiffer = AsyncPagedListDiffer(object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + notifyItemRangeInserted(position, count) + if (position == 0) { + topLoadedCallback() + } + } + + override fun onRemoved(position: Int, count: Int) { + notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + notifyItemRangeChanged(position, count, payload) + } + }, AsyncDifferConfig.Builder(CONVERSATION_COMPARATOR).build()) + + fun submitList(list: PagedList) { + differ.submitList(list) + } + + 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) + else -> throw IllegalArgumentException("unknown view type $viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (getItemViewType(position)) { + R.layout.item_network_state -> (holder as NetworkStateViewHolder).setUpWithNetworkState(networkState, differ.itemCount == 0) + R.layout.item_conversation -> (holder as ConversationViewHolder).setupWithConversation(differ.getItem(position)) + } + } + + private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED + + override fun getItemViewType(position: Int): Int { + return if (hasExtraRow() && position == itemCount - 1) { + R.layout.item_network_state + } else { + R.layout.item_conversation + } + } + + override fun getItemCount(): Int { + return differ.itemCount + if (hasExtraRow()) 1 else 0 + } + + fun setNetworkState(newNetworkState: NetworkState?) { + val previousState = this.networkState + val hadExtraRow = hasExtraRow() + this.networkState = newNetworkState + val hasExtraRow = hasExtraRow() + if (hadExtraRow != hasExtraRow) { + if (hadExtraRow) { + notifyItemRemoved(differ.itemCount) + } else { + notifyItemInserted(differ.itemCount) + } + } else if (hasExtraRow && previousState != newNetworkState) { + notifyItemChanged(itemCount - 1) + } + } + + companion object { + + val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean = + oldItem == newItem + + override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean = + oldItem.id == newItem.id + } + + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..7c4ed10 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -0,0 +1,194 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation + +import android.text.Spanned +import android.text.SpannedString +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.util.shouldTrimStatus +import java.util.* + +@Entity(primaryKeys = ["id","accountId"]) +@TypeConverters(Converters::class) +data class ConversationEntity( + val accountId: Long, + val id: String, + val accounts: List, + val unread: Boolean, + @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity +) + +data class ConversationAccountEntity( + val id: String, + val username: String, + val displayName: String, + val avatar: String, + val emojis: List +) { + fun toAccount(): Account { + return Account( + id = id, + username = username, + displayName = displayName, + avatar = avatar, + emojis = emojis, + url = "", + localUsername = "", + note = SpannedString(""), + header = "" + ) + } +} + +@TypeConverters(Converters::class) +data class ConversationStatusEntity( + val id: String, + val url: String?, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val account: ConversationAccountEntity, + val content: Spanned, + val createdAt: Date, + val emojis: List, + val favouritesCount: Int, + val favourited: Boolean, + val bookmarked: Boolean, + val sensitive: Boolean, + val spoilerText: String, + val attachments: ArrayList, + val mentions: Array, + val showingHiddenContent: Boolean, + val expanded: Boolean, + val collapsible: Boolean, + val collapsed: Boolean, + val poll: Poll? + +) { + /** its necessary to override this because Spanned.equals does not work as expected */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ConversationStatusEntity + + if (id != other.id) return false + if (url != other.url) return false + if (inReplyToId != other.inReplyToId) return false + if (inReplyToAccountId != other.inReplyToAccountId) return false + if (account != other.account) return false + if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings + if (createdAt != other.createdAt) return false + if (emojis != other.emojis) return false + if (favouritesCount != other.favouritesCount) return false + if (favourited != other.favourited) return false + if (sensitive != other.sensitive) return false + if (spoilerText != other.spoilerText) return false + if (attachments != other.attachments) return false + if (!mentions.contentEquals(other.mentions)) return false + if (showingHiddenContent != other.showingHiddenContent) return false + if (expanded != other.expanded) return false + if (collapsible != other.collapsible) return false + if (collapsed != other.collapsed) return false + if (poll != other.poll) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + (url?.hashCode() ?: 0) + result = 31 * result + (inReplyToId?.hashCode() ?: 0) + result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) + result = 31 * result + account.hashCode() + result = 31 * result + content.hashCode() + result = 31 * result + createdAt.hashCode() + result = 31 * result + emojis.hashCode() + result = 31 * result + favouritesCount + result = 31 * result + favourited.hashCode() + result = 31 * result + sensitive.hashCode() + result = 31 * result + spoilerText.hashCode() + result = 31 * result + attachments.hashCode() + result = 31 * result + mentions.contentHashCode() + result = 31 * result + showingHiddenContent.hashCode() + result = 31 * result + expanded.hashCode() + result = 31 * result + collapsible.hashCode() + result = 31 * result + collapsed.hashCode() + result = 31 * result + poll.hashCode() + return result + } + + fun toStatus(): Status { + return Status( + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive= sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + application = null, + pinned = false, + poll = poll, + card = null) + } +} + +fun Account.toEntity() = + ConversationAccountEntity( + id, + username, + displayName.orEmpty(), + avatar, + emojis ?: emptyList() + ) + +fun Status.toEntity() = + ConversationStatusEntity( + id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content, + createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive, + spoilerText, attachments, mentions, + false, + false, + shouldTrimStatus(content), + true, + poll + ) + + +fun Conversation.toEntity(accountId: Long) = + ConversationEntity( + accountId, + id, + accounts.map { it.toEntity() }, + unread, + lastStatus!!.toEntity() + ) 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 new file mode 100644 index 0000000..19ef749 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -0,0 +1,168 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation; + +import android.content.Context; +import android.text.InputFilter; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.ImageLoadingHelper; +import com.keylesspalace.tusky.util.SmartLengthInputFilter; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; + +import java.util.List; + +public class ConversationViewHolder extends StatusBaseViewHolder { + private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; + + private TextView conversationNameTextView; + private Button contentCollapseButton; + private ImageView[] avatars; + + private StatusDisplayOptions statusDisplayOptions; + private StatusActionListener listener; + + ConversationViewHolder(View itemView, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { + super(itemView); + conversationNameTextView = itemView.findViewById(R.id.conversation_name); + contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); + avatars = new ImageView[]{ + avatar, + itemView.findViewById(R.id.status_avatar_1), + itemView.findViewById(R.id.status_avatar_2) + }; + this.statusDisplayOptions = statusDisplayOptions; + + this.listener = listener; + + } + + @Override + protected int getMediaPreviewHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); + } + + void setupWithConversation(ConversationEntity conversation) { + ConversationStatusEntity status = conversation.getLastStatus(); + ConversationAccountEntity account = status.getAccount(); + + setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); + + setDisplayName(account.getDisplayName(), account.getEmojis()); + setUsername(account.getUsername()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + setIsReply(status.getInReplyToId() != null); + setFavourited(status.getFavourited()); + setBookmarked(status.getBookmarked()); + List attachments = status.getAttachments(); + boolean sensitive = status.getSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), + statusDisplayOptions.useBlurhash()); + + if (attachments.size() == 0) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent()); + // Hide all unused views. + mediaPreviews[0].setVisibility(View.GONE); + mediaPreviews[1].setVisibility(View.GONE); + mediaPreviews[2].setVisibility(View.GONE); + mediaPreviews[3].setVisibility(View.GONE); + hideSensitiveMediaWarning(); + } + + setupButtons(listener, account.getId(), status.getContent().toString(), + statusDisplayOptions); + + setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), + status.getMentions(), status.getEmojis(), + PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); + + setConversationName(conversation.getAccounts()); + + setAvatars(conversation.getAccounts()); + } + + private void setConversationName(List accounts) { + Context context = conversationNameTextView.getContext(); + String conversationName = ""; + if (accounts.size() == 1) { + conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername()); + } else if (accounts.size() == 2) { + conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername()); + } else if (accounts.size() > 2) { + conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2); + } + + conversationNameTextView.setText(conversationName); + } + + private void setAvatars(List accounts) { + for (int i = 0; i < avatars.length; i++) { + ImageView avatarView = avatars[i]; + if (i < accounts.size()) { + ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, + avatarRadius48dp, statusDisplayOptions.animateAvatars()); + avatarView.setVisibility(View.VISIBLE); + } else { + avatarView.setVisibility(View.GONE); + } + } + } + + private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean expanded, String spoilerText, final StatusActionListener listener) { + /* input filter for TextViews have to be set before text */ + if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { + contentCollapseButton.setOnClickListener(view -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) + listener.onContentCollapsedChange(!collapsed, position); + }); + + contentCollapseButton.setVisibility(View.VISIBLE); + if (collapsed) { + contentCollapseButton.setText(R.string.status_content_warning_show_more); + content.setFilters(COLLAPSE_INPUT_FILTER); + } else { + contentCollapseButton.setText(R.string.status_content_warning_show_less); + content.setFilters(NO_INPUT_FILTER); + } + } else { + contentCollapseButton.setVisibility(View.GONE); + content.setFilters(NO_INPUT_FILTER); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt new file mode 100644 index 0000000..5d35901 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.keylesspalace.tusky.components.conversation + +import androidx.annotation.MainThread +import androidx.paging.PagedList +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.PagingRequestHelper +import com.keylesspalace.tusky.util.createStatusLiveData +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.concurrent.Executor + +/** + * This boundary callback gets notified when user reaches to the edges of the list such that the + * database cannot provide any more data. + *

+ * The boundary callback might be called multiple times for the same direction so it does its own + * rate limiting using the PagingRequestHelper class. + */ +class ConversationsBoundaryCallback( + private val accountId: Long, + private val mastodonApi: MastodonApi, + private val handleResponse: (Long, List?) -> Unit, + private val ioExecutor: Executor, + private val networkPageSize: Int) + : PagedList.BoundaryCallback() { + + val helper = PagingRequestHelper(ioExecutor) + val networkState = helper.createStatusLiveData() + + /** + * Database returned 0 items. We should query the backend for more items. + */ + @MainThread + override fun onZeroItemsLoaded() { + helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { + mastodonApi.getConversations(null, networkPageSize) + .enqueue(createWebserviceCallback(it)) + } + } + + /** + * User reached to the end of the list. + */ + @MainThread + override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) { + helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { + mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize) + .enqueue(createWebserviceCallback(it)) + } + } + + /** + * every time it gets new items, boundary callback simply inserts them into the database and + * paging library takes care of refreshing the list if necessary. + */ + private fun insertItemsIntoDb( + response: Response>, + it: PagingRequestHelper.Request.Callback) { + ioExecutor.execute { + handleResponse(accountId, response.body()) + it.recordSuccess() + } + } + + override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) { + // ignored, since we only ever append to what's in the DB + } + + private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback> { + return object : Callback> { + override fun onFailure(call: Call>, t: Throwable) { + it.recordFailure(t) + } + + override fun onResponse(call: Call>, response: Response>) { + insertItemsIntoDb(response, it) + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..48926a6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -0,0 +1,204 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.paging.PagedList +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import com.keylesspalace.tusky.AccountActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import kotlinx.android.synthetic.main.fragment_timeline.* +import javax.inject.Inject + +class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } + + private lateinit var adapter: ConversationAdapter + + private var layoutManager: LinearLayoutManager? = null + + 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?) { + val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) + + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = preferences.getBoolean("showBotOverlay", true), + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + renderStatusAsMention = preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + ) + + adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) + + recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + layoutManager = LinearLayoutManager(view.context) + recyclerView.layoutManager = layoutManager + recyclerView.adapter = adapter + (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + progressBar.hide() + statusView.hide() + + initSwipeToRefresh() + + viewModel.conversations.observe(viewLifecycleOwner, Observer> { + adapter.submitList(it) + }) + viewModel.networkState.observe(viewLifecycleOwner, Observer { + adapter.setNetworkState(it) + }) + + viewModel.load() + + } + + private fun initSwipeToRefresh() { + viewModel.refreshState.observe(viewLifecycleOwner, Observer { + swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING + }) + swipeRefreshLayout.setOnRefreshListener { + viewModel.refresh() + } + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + + private fun onTopLoaded() { + recyclerView.scrollToPosition(0) + } + + override fun onReblog(reblog: Boolean, position: Int) { + // its impossible to reblog private messages + } + + override fun onFavourite(favourite: Boolean, position: Int) { + viewModel.favourite(favourite, position) + } + + override fun onBookmark(favourite: Boolean, position: Int) { + viewModel.bookmark(favourite, position) + } + + override fun onMore(view: View, position: Int) { + viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { + more(it.toStatus(), view, position) + } + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { + viewMedia(attachmentIndex, it.toStatus(), view) + } + } + + override fun onViewThread(position: Int) { + viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { + viewThread(it.toStatus()) + } + } + + override fun onViewReplyTo(position: Int) { + // there are no Reply to labels in conversations + } + + override fun onOpenReblog(position: Int) { + // there are no reblogs in search results + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + viewModel.expandHiddenStatus(expanded, position) + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + viewModel.showContent(isShowing, position) + } + + override fun onLoadMore(position: Int) { + // not using the old way of pagination + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + viewModel.collapseLongStatus(isCollapsed, position) + } + + override fun onViewAccount(id: String) { + val intent = AccountActivity.getIntent(requireContext(), id) + startActivity(intent) + } + + override fun onViewTag(tag: String) { + val intent = Intent(context, ViewTagActivity::class.java) + intent.putExtra("hashtag", tag) + startActivity(intent) + } + + override fun removeItem(position: Int) { + viewModel.remove(position) + } + + override fun onReply(position: Int) { + viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { + reply(it.toStatus()) + } + } + + private fun jumpToTop() { + if (isAdded) { + layoutManager?.scrollToPosition(0) + recyclerView.stopScroll() + } + } + + override fun onReselect() { + jumpToTop() + } + + override fun onVoteInPoll(position: Int, choices: MutableList) { + viewModel.voteInPoll(position, choices) + } + + companion object { + fun newInstance() = ConversationsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt new file mode 100644 index 0000000..3cb4745 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -0,0 +1,111 @@ +package com.keylesspalace.tusky.components.conversation + +import androidx.annotation.MainThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.paging.Config +import androidx.paging.toLiveData +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Listing +import com.keylesspalace.tusky.util.NetworkState +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.concurrent.Executors +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) { + + private val ioExecutor = Executors.newSingleThreadExecutor() + + companion object { + private const val DEFAULT_PAGE_SIZE = 20 + } + + @MainThread + fun refresh(accountId: Long, showLoadingIndicator: Boolean): LiveData { + val networkState = MutableLiveData() + if(showLoadingIndicator) { + networkState.value = NetworkState.LOADING + } + + mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue( + object : Callback> { + override fun onFailure(call: Call>, t: Throwable) { + // retrofit calls this on main thread so safe to call set value + networkState.value = NetworkState.error(t.message) + } + + override fun onResponse(call: Call>, response: Response>) { + ioExecutor.execute { + db.runInTransaction { + db.conversationDao().deleteForAccount(accountId) + insertResultIntoDb(accountId, response.body()) + } + // since we are in bg thread now, post the result. + networkState.postValue(NetworkState.LOADED) + } + } + } + ) + return networkState + } + + @MainThread + fun conversations(accountId: Long): Listing { + // create a boundary callback which will observe when the user reaches to the edges of + // the list and update the database with extra data. + val boundaryCallback = ConversationsBoundaryCallback( + accountId = accountId, + mastodonApi = mastodonApi, + handleResponse = this::insertResultIntoDb, + ioExecutor = ioExecutor, + networkPageSize = DEFAULT_PAGE_SIZE) + // we are using a mutable live data to trigger refresh requests which eventually calls + // refresh method and gets a new live data. Each refresh request by the user becomes a newly + // dispatched data in refreshTrigger + val refreshTrigger = MutableLiveData() + val refreshState = Transformations.switchMap(refreshTrigger) { + refresh(accountId, true) + } + + // We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder + val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData( + config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false), + boundaryCallback = boundaryCallback + ) + + return Listing( + pagedList = livePagedList, + networkState = boundaryCallback.networkState, + retry = { + boundaryCallback.helper.retryAllFailed() + }, + refresh = { + refreshTrigger.value = null + }, + refreshState = refreshState + ) + } + + fun deleteCacheForAccount(accountId: Long) { + Single.fromCallable { + db.conversationDao().deleteForAccount(accountId) + }.subscribeOn(Schedulers.io()) + .subscribe() + } + + private fun insertResultIntoDb(accountId: Long, result: List?) { + result?.filter { it.lastStatus != null } + ?.map{ it.toEntity(accountId) } + ?.let { db.conversationDao().insert(it) } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt new file mode 100644 index 0000000..c6fa84b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -0,0 +1,142 @@ +package com.keylesspalace.tusky.components.conversation + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.paging.PagedList +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.util.Listing +import com.keylesspalace.tusky.util.NetworkState +import com.keylesspalace.tusky.util.RxAwareViewModel +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class ConversationsViewModel @Inject constructor( + private val repository: ConversationsRepository, + private val timelineCases: TimelineCases, + private val database: AppDatabase, + private val accountManager: AccountManager +) : RxAwareViewModel() { + + private val repoResult = MutableLiveData>() + + val conversations: LiveData> = Transformations.switchMap(repoResult) { it.pagedList } + val networkState: LiveData = Transformations.switchMap(repoResult) { it.networkState } + val refreshState: LiveData = Transformations.switchMap(repoResult) { it.refreshState } + + fun load() { + val accountId = accountManager.activeAccount?.id ?: return + if (repoResult.value == null) { + repository.refresh(accountId, false) + } + repoResult.value = repository.conversations(accountId) + } + + fun refresh() { + repoResult.value?.refresh?.invoke() + } + + fun retry() { + repoResult.value?.retry?.invoke() + } + + fun favourite(favourite: Boolean, position: Int) { + conversations.value?.getOrNull(position)?.let { conversation -> + timelineCases.favourite(conversation.lastStatus.toStatus(), favourite) + .flatMap { + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(favourited = favourite) + ) + + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() + } + + } + + fun bookmark(bookmark: Boolean, position: Int) { + conversations.value?.getOrNull(position)?.let { conversation -> + timelineCases.bookmark(conversation.lastStatus.toStatus(), bookmark) + .flatMap { + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + ) + + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> Log.w("ConversationViewModel", "Failed to bookmark conversation", t) } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() + } + + } + + fun voteInPoll(position: Int, choices: MutableList) { + conversations.value?.getOrNull(position)?.let { conversation -> + timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices) + .flatMap { poll -> + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(poll = poll) + ) + + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() + } + + } + + fun expandHiddenStatus(expanded: Boolean, position: Int) { + conversations.value?.getOrNull(position)?.let { conversation -> + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(expanded = expanded) + ) + saveConversationToDb(newConversation) + } + } + + fun collapseLongStatus(collapsed: Boolean, position: Int) { + conversations.value?.getOrNull(position)?.let { conversation -> + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(collapsed = collapsed) + ) + saveConversationToDb(newConversation) + } + } + + fun showContent(showing: Boolean, position: Int) { + conversations.value?.getOrNull(position)?.let { conversation -> + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) + ) + saveConversationToDb(newConversation) + } + } + + fun remove(position: Int) { + conversations.value?.getOrNull(position)?.let { + refresh() + } + } + + private fun saveConversationToDb(conversation: ConversationEntity) { + database.conversationDao().insert(conversation) + .subscribeOn(Schedulers.io()) + .subscribe() + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt new file mode 100644 index 0000000..e1fc70f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -0,0 +1,161 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.net.Uri +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftAttachment +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.IOUtils +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.File +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +class DraftHelper @Inject constructor( + val context: Context, + db: AppDatabase +) { + + private val draftDao = db.draftDao() + + fun saveDraft( + draftId: Int, + accountId: Long, + inReplyToId: String?, + content: String?, + contentWarning: String?, + sensitive: Boolean, + visibility: Status.Visibility, + mediaUris: List, + mediaDescriptions: List, + poll: NewPoll?, + formattingSyntax: String, + failedToSend: Boolean + ): Completable { + return Single.fromCallable { + + val draftDirectory = context.getExternalFilesDir("Tusky") + + if (draftDirectory == null || !(draftDirectory.exists())) { + Log.e("DraftHelper", "Error obtaining directory to save media.") + throw Exception() + } + + val uris = mediaUris.map { uriString -> + uriString.toUri() + }.map { uri -> + if (uri.isNotInFolder(draftDirectory)) { + uri.copyToFolder(draftDirectory) + } else { + uri + } + } + + val types = uris.map { uri -> + val mimeType = context.contentResolver.getType(uri) + when (mimeType?.substring(0, mimeType.indexOf('/'))) { + "video" -> DraftAttachment.Type.VIDEO + "image" -> DraftAttachment.Type.IMAGE + "audio" -> DraftAttachment.Type.AUDIO + else -> throw IllegalStateException("unknown media type") + } + } + + val attachments: MutableList = mutableListOf() + for (i in mediaUris.indices) { + attachments.add( + DraftAttachment( + uriString = uris[i].toString(), + description = mediaDescriptions[i], + type = types[i] + ) + ) + } + + DraftEntity( + id = draftId, + accountId = accountId, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = sensitive, + visibility = visibility, + attachments = attachments, + poll = poll, + formattingSyntax = formattingSyntax, + failedToSend = failedToSend + ) + + }.flatMapCompletable { draft -> + draftDao.insertOrReplace(draft) + }.subscribeOn(Schedulers.io()) + } + + fun deleteDraftAndAttachments(draftId: Int): Completable { + return draftDao.find(draftId) + .flatMapCompletable { draft -> + deleteDraftAndAttachments(draft) + } + } + + fun deleteDraftAndAttachments(draft: DraftEntity): Completable { + return deleteAttachments(draft) + .andThen(draftDao.delete(draft.id)) + } + + fun deleteAttachments(draft: DraftEntity): Completable { + return Completable.fromCallable { + draft.attachments.forEach { attachment -> + if (context.contentResolver.delete(attachment.uri, null, null) == 0) { + Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") + } + } + }.subscribeOn(Schedulers.io()) + } + + private fun Uri.isNotInFolder(folder: File): Boolean { + val filePath = path ?: return true + return File(filePath).parentFile == folder + } + + private fun Uri.copyToFolder(folder: File): Uri { + val contentResolver = context.contentResolver + + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + + val mimeType = contentResolver.getType(this) + val map = MimeTypeMap.getSingleton() + val fileExtension = map.getExtensionFromMimeType(mimeType) + + val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension) + val file = File(folder, filename) + IOUtils.copyToFile(contentResolver, this, file) + return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt new file mode 100644 index 0000000..69403fd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -0,0 +1,81 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.DraftAttachment + +class DraftMediaAdapter( + private val attachmentClick: () -> Unit +) : ListAdapter( + object: DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { + return DraftMediaViewHolder(AppCompatImageView(parent.context)) + } + + override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) { + getItem(position)?.let { attachment -> + if (attachment.type == DraftAttachment.Type.AUDIO) { + holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else { + Glide.with(holder.itemView.context) + .load(attachment.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.imageView) + } + } + } + + inner class DraftMediaViewHolder(val imageView: ImageView) + : RecyclerView.ViewHolder(imageView) { + init { + val thumbnailViewSize = + imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + imageView.layoutParams = layoutParams + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + imageView.setOnClickListener { + attachmentClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt new file mode 100644 index 0000000..b1255e8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -0,0 +1,199 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.SavedTootActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityDraftsBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import retrofit2.HttpException +import javax.inject.Inject + +class DraftsActivity : BaseActivity(), DraftActionListener { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: DraftsViewModel by viewModels { viewModelFactory } + + private lateinit var binding: ActivityDraftsBinding + private lateinit var bottomSheet: BottomSheetBehavior + + private var oldDraftsButton: MenuItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + + binding = ActivityDraftsBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_drafts) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status) + + val adapter = DraftsAdapter(this) + + binding.draftsRecyclerView.adapter = adapter + binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) + + viewModel.drafts.observe(this) { draftList -> + if (draftList.isEmpty()) { + binding.draftsRecyclerView.hide() + binding.draftsErrorMessageView.show() + } else { + binding.draftsRecyclerView.show() + binding.draftsErrorMessageView.hide() + adapter.submitList(draftList) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.drafts, menu) + oldDraftsButton = menu.findItem(R.id.action_old_drafts) + viewModel.showOldDraftsButton() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { showOldDraftsButton -> + oldDraftsButton?.isVisible = showOldDraftsButton + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.action_old_drafts -> { + val intent = Intent(this, SavedTootActivity::class.java) + startActivityWithSlideInAnimation(intent) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onOpenDraft(draft: DraftEntity) { + + if (draft.inReplyToId != null) { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.getToot(draft.inReplyToId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe({ status -> + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility, + formattingSyntax = draft.formattingSyntax + ) + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + + }, { throwable -> + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + Log.w(TAG, "failed loading reply information", throwable) + + if (throwable is HttpException && throwable.code() == 404) { + // the original status to which a reply was drafted has been deleted + // let's open the ComposeActivity without reply information + Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show() + openDraftWithoutReply(draft) + } else { + Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) + .show() + } + }) + } else { + openDraftWithoutReply(draft) + } + } + + private fun openDraftWithoutReply(draft: DraftEntity) { + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility, + formattingSyntax = draft.formattingSyntax + ) + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + } + + override fun onDeleteDraft(draft: DraftEntity) { + viewModel.deleteDraft(draft) + Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + viewModel.restoreDraft(draft) + } + .show() + } + + companion object { + const val TAG = "DraftsActivity" + + fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt new file mode 100644 index 0000000..5dfbcea --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -0,0 +1,92 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemDraftBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.util.BindingViewHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible + +interface DraftActionListener { + fun onOpenDraft(draft: DraftEntity) + fun onDeleteDraft(draft: DraftEntity) +} + +class DraftsAdapter( + private val listener: DraftActionListener +) : PagedListAdapter>( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + + val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + val viewHolder = BindingViewHolder(binding) + + binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) + binding.draftMediaPreview.adapter = DraftMediaAdapter { + getItem(viewHolder.adapterPosition)?.let { draft -> + listener.onOpenDraft(draft) + } + } + + return viewHolder + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + getItem(position)?.let { draft -> + holder.binding.root.setOnClickListener { + listener.onOpenDraft(draft) + } + holder.binding.deleteButton.setOnClickListener { + listener.onDeleteDraft(draft) + } + holder.binding.draftSendingInfo.visible(draft.failedToSend) + + holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty()) + holder.binding.contentWarning.text = draft.contentWarning + holder.binding.content.text = draft.content + + holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty()) + (holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments) + + if (draft.poll != null) { + holder.binding.draftPoll.show() + holder.binding.draftPoll.setPoll(draft.poll) + } else { + holder.binding.draftPoll.hide() + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt new file mode 100644 index 0000000..9eca963 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -0,0 +1,69 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import androidx.lifecycle.ViewModel +import androidx.paging.toLiveData +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.Observable +import io.reactivex.Single +import javax.inject.Inject + +class DraftsViewModel @Inject constructor( + val database: AppDatabase, + val accountManager: AccountManager, + val api: MastodonApi, + val draftHelper: DraftHelper +) : ViewModel() { + + val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20) + + private val deletedDrafts: MutableList = mutableListOf() + + fun showOldDraftsButton(): Observable { + return database.tootDao().savedTootCount() + .map { count -> count > 0 } + } + + fun deleteDraft(draft: DraftEntity) { + // this does not immediately delete media files to avoid unnecessary file operations + // in case the user decides to restore the draft + database.draftDao().delete(draft.id) + .subscribe() + deletedDrafts.add(draft) + } + + fun restoreDraft(draft: DraftEntity) { + database.draftDao().insertOrReplace(draft) + .subscribe() + deletedDrafts.remove(draft) + } + + fun getToot(tootId: String): Single { + return api.statusSingle(tootId) + } + + override fun onCleared() { + deletedDrafts.forEach { + draftHelper.deleteAttachments(it).subscribe() + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt new file mode 100644 index 0000000..f4505ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -0,0 +1,47 @@ +package com.keylesspalace.tusky.components.instancemute + +import android.os.Bundle +import android.view.MenuItem +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import javax.inject.Inject +import kotlinx.android.synthetic.main.toolbar_basic.* + +class InstanceListActivity: BaseActivity(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_account_list) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + setTitle(R.string.title_domain_mutes) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, InstanceListFragment()) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun androidInjector() = androidInjector + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt new file mode 100644 index 0000000..62ab7ef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -0,0 +1,57 @@ +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.* + +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 onBindViewHolder(holder: ViewHolder, position: Int) { + holder.setupWithInstance(instances[position]) + } + + override fun getItemCount(): Int { + var count = instances.size + if (bottomLoading) + ++count + return count + } + + fun addItems(newInstances: List) { + val end = instances.size + instances.addAll(newInstances) + notifyItemRangeInserted(end, instances.size) + } + + fun addItem(instance: String) { + instances.add(instance) + notifyItemInserted(instances.size) + } + + 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/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt new file mode 100644 index 0000000..bb850bd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -0,0 +1,179 @@ +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.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +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.di.Injectable +import com.keylesspalace.tusky.fragment.BaseFragment +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +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 +import java.io.IOException +import javax.inject.Inject + +class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { + @Inject + lateinit var api: MastodonApi + + private var fetching = false + private var bottomId: String? = null + private var adapter = DomainMutesAdapter(this) + private lateinit var scrollListener: EndlessOnScrollListener + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment_instance_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.setHasFixedSize(true) + recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + recyclerView.adapter = adapter + + val layoutManager = LinearLayoutManager(view.context) + recyclerView.layoutManager = layoutManager + + scrollListener = object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + if (bottomId != null) { + fetchInstances(bottomId) + } + } + } + + recyclerView.addOnScrollListener(scrollListener) + fetchInstances() + } + + override fun mute(mute: Boolean, instance: String, position: Int) { + if (mute) { + api.blockDomain(instance).enqueue(object: Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error muting domain $instance") + } + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + adapter.addItem(instance) + } else { + Log.e(TAG, "Error muting domain $instance") + } + } + }) + } else { + api.unblockDomain(instance).enqueue(object: Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error unmuting domain $instance") + } + + 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) + .setAction(R.string.action_undo) { + mute(true, instance, position) + } + .show() + } else { + Log.e(TAG, "Error unmuting domain $instance") + } + } + }) + } + } + + private fun fetchInstances(id: String? = null) { + if (fetching) { + return + } + fetching = true + instanceProgressBar.show() + + if (id != null) { + recyclerView.post { adapter.bottomLoading = true } + } + + api.domainBlocks(id, bottomId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ response -> + val instances = response.body() + + if (response.isSuccessful && instances != null) { + onFetchInstancesSuccess(instances, response.headers().get("Link")) + } else { + onFetchInstancesFailure(Exception(response.message())) + } + }, {throwable -> + onFetchInstancesFailure(throwable) + }) + } + + private fun onFetchInstancesSuccess(instances: List, linkHeader: String?) { + adapter.bottomLoading = false + instanceProgressBar.hide() + + val links = HttpHeaderLink.parse(linkHeader) + val next = HttpHeaderLink.findByRelationType(links, "next") + val fromId = next?.uri?.getQueryParameter("max_id") + adapter.addItems(instances) + bottomId = fromId + fetching = false + + if (adapter.itemCount == 0) { + messageView.show() + messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } else { + messageView.hide() + } + } + + private fun onFetchInstancesFailure(throwable: Throwable) { + fetching = false + instanceProgressBar.hide() + Log.e(TAG, "Fetch failure", throwable) + + if (adapter.itemCount == 0) { + messageView.show() + if (throwable is IOException) { + messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + messageView.hide() + this.fetchInstances(null) + } + } else { + messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + messageView.hide() + this.fetchInstances(null) + } + } + } + } + + companion object { + private const val TAG = "InstanceList" // logging tag + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt new file mode 100644 index 0000000..97d59cc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt @@ -0,0 +1,5 @@ +package com.keylesspalace.tusky.components.instancemute.interfaces + +interface InstanceActionListener { + fun mute(mute: Boolean, instance: String, position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt new file mode 100644 index 0000000..0507d5c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt @@ -0,0 +1,83 @@ +package com.keylesspalace.tusky.components.notifications + +import android.util.Log +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Marker +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isLessThan +import javax.inject.Inject + +class NotificationFetcher @Inject constructor( + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val notifier: Notifier +) { + fun fetchAndShow() { + for (account in accountManager.getAllAccountsOrderedByActive()) { + if (account.notificationsEnabled) { + try { + val notifications = fetchNotifications(account) + notifications.forEachIndexed { index, notification -> + notifier.show(notification, account, index == 0) + } + accountManager.saveAccount(account) + } catch (e: Exception) { + Log.w(TAG, "Error while fetching notifications", e) + } + } + } + } + + private fun fetchNotifications(account: AccountEntity): MutableList { + val authHeader = String.format("Bearer %s", account.accessToken) + // We fetch marker to not load/show notifications which user has already seen + val marker = fetchMarker(authHeader, account) + if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) { + account.lastNotificationId = marker.lastReadId + } + Log.d(TAG, "getting Notifications for " + account.fullName) + val notifications = mastodonApi.notificationsWithAuth( + authHeader, + account.domain, + account.lastNotificationId, + Notification.Type.asStringList + ).blockingGet() + + val newId = account.lastNotificationId + var newestId = "" + val result = mutableListOf() + for (notification in notifications.reversed()) { + val currentId = notification.id + if (newestId.isLessThan(currentId)) { + newestId = currentId + account.lastNotificationId = currentId + } + if (newId.isLessThan(currentId)) { + result.add(notification) + } + } + return result + } + + private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? { + return try { + val allMarkers = mastodonApi.markersWithAuth( + authHeader, + account.domain, + listOf("notifications") + ).blockingGet() + val notificationMarker = allMarkers["notifications"] + Log.d(TAG, "Fetched marker: $notificationMarker") + notificationMarker + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch marker", e) + null + } + } + + companion object { + const val TAG = "NotificationFetcher" + } +} 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 new file mode 100644 index 0000000..8cc5900 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -0,0 +1,763 @@ +/* Copyright 2018 Jeremiasz Nelz + * Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.notifications; + +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.RemoteInput; +import androidx.core.app.TaskStackBuilder; +import androidx.core.content.ContextCompat; +import androidx.work.Constraints; +import androidx.work.NetworkType; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.FutureTarget; +import com.keylesspalace.tusky.BuildConfig; +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.db.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.entity.ChatMessage; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Poll; +import com.keylesspalace.tusky.entity.PollOption; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver; +import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; +import com.keylesspalace.tusky.util.StringUtils; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Single; +import io.reactivex.schedulers.Schedulers; + +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + +public class NotificationHelper { + + private static int notificationId = 0; + + /** + * constants used in Intents + */ + public static final String ACCOUNT_ID = "account_id"; + + private static final String TAG = "NotificationHelper"; + + public static final String REPLY_ACTION = "REPLY_ACTION"; + + public static final String COMPOSE_ACTION = "COMPOSE_ACTION"; + + public static final String CHAT_REPLY_ACTION = "CHAT_REPLY_ACTION"; + + public static final String KEY_REPLY = "KEY_REPLY"; + + public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID"; + + public static final String KEY_SENDER_ACCOUNT_IDENTIFIER = "KEY_SENDER_ACCOUNT_IDENTIFIER"; + + public static final String KEY_SENDER_ACCOUNT_FULL_NAME = "KEY_SENDER_ACCOUNT_FULL_NAME"; + + public static final String KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"; + + public static final String KEY_CITED_STATUS_ID = "KEY_CITED_STATUS_ID"; + + public static final String KEY_VISIBILITY = "KEY_VISIBILITY"; + + public static final String KEY_SPOILER = "KEY_SPOILER"; + + public static final String KEY_MENTIONS = "KEY_MENTIONS"; + + public static final String KEY_CITED_TEXT = "KEY_CITED_TEXT"; + + public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL"; + + public static final String KEY_CHAT_ID = "KEY_CHAT_ID"; + + /** + * notification channels used on Android O+ + **/ + public static final String CHANNEL_MENTION = "CHANNEL_MENTION"; + public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW"; + public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST"; + public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; + public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; + public static final String CHANNEL_POLL = "CHANNEL_POLL"; + public static final String CHANNEL_EMOJI_REACTION = "CHANNEL_EMOJI_REACTION"; + public static final String CHANNEL_CHAT_MESSAGES = "CHANNEL_CHAT_MESSAGES"; + public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; + public static final String CHANNEL_MOVE = "CHANNEL_MOVE"; + + /** + * WorkManager Tag + */ + private static final String NOTIFICATION_PULL_TAG = "pullNotifications"; + + /** + * by setting this as false, it's possible to test legacy notification channels on newer devices + */ + // public static final boolean NOTIFICATION_USE_CHANNELS = false; + public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + /** + * Takes a given Mastodon notification and either creates a new Android notification or updates + * the state of the existing notification to reflect the new interaction. + * + * @param context to access application preferences and services + * @param body a new Mastodon notification + * @param account the account for which the notification should be shown + */ + + public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) { + body = Notification.rewriteToStatusTypeIfNeeded(body, account.getAccountId()); + + if (!filterNotification(account, body, context)) { + return; + } + + // Pleroma extension: don't notify about seen notifications + if (body.getPleroma() != null && body.getPleroma().getSeen()) { + return; + } + + if (body.getStatus() != null && + (body.getStatus().isUserMuted() || + body.getStatus().isThreadMuted())) { + return; + } + + String rawCurrentNotifications = account.getActiveNotifications(); + JSONArray currentNotifications; + + try { + currentNotifications = new JSONArray(rawCurrentNotifications); + } catch (JSONException e) { + currentNotifications = new JSONArray(); + } + + for (int i = 0; i < currentNotifications.length(); i++) { + try { + if (currentNotifications.getString(i).equals(body.getAccount().getName())) { + currentNotifications.remove(i); + break; + } + } catch (JSONException e) { + Log.d(TAG, Log.getStackTraceString(e)); + } + } + + currentNotifications.put(body.getAccount().getName()); + + account.setActiveNotifications(currentNotifications.toString()); + + // Notification group member + // ========================= + final NotificationCompat.Builder builder = newNotification(context, body, account, false); + + notificationId++; + + builder.setContentTitle(titleForType(context, body, account)) + .setContentText(bodyForType(body, context)); + + if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) { + builder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(bodyForType(body, context))); + } + + //load the avatar synchronously + Bitmap accountAvatar; + try { + FutureTarget target = Glide.with(context) + .asBitmap() + .load(body.getAccount().getAvatar()) + .transform(new RoundedCorners(20)) + .submit(); + + accountAvatar = target.get(); + } catch (ExecutionException | InterruptedException e) { + Log.d(TAG, "error loading account avatar", e); + accountAvatar = BitmapFactory.decodeResource(context.getResources(), R.drawable.avatar_default); + } + + builder.setLargeIcon(accountAvatar); + + // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat + if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if(body.getType() == Notification.Type.MENTION) { + RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); + + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); + + NotificationCompat.Action quickReplyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + .addRemoteInput(replyRemoteInput) + .build(); + + builder.addAction(quickReplyAction); + + PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); + + NotificationCompat.Action composeAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_compose_shortcut), composePendingIntent) + .build(); + + builder.addAction(composeAction); + } else if(body.getType() == Notification.Type.CHAT_MESSAGE) { + RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); + + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(CHAT_REPLY_ACTION, context, body, account); + + NotificationCompat.Action quickReplyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + .addRemoteInput(replyRemoteInput) + .build(); + + builder.addAction(quickReplyAction); + } + } + + builder.setSubText(account.getFullName()); + builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); + builder.setOnlyAlertOnce(true); + + // only alert for the first notification of a batch to avoid multiple alerts at once + if(!isFirstOfBatch) { + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + } + + // Summary + // ======= + final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true); + + if (currentNotifications.length() != 1) { + try { + String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); + String text = joinNames(context, currentNotifications); + summaryBuilder.setContentTitle(title) + .setContentText(text); + } catch (JSONException e) { + Log.d(TAG, Log.getStackTraceString(e)); + } + } + + summaryBuilder.setSubText(account.getFullName()); + summaryBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); + summaryBuilder.setCategory(NotificationCompat.CATEGORY_SOCIAL); + summaryBuilder.setOnlyAlertOnce(true); + summaryBuilder.setGroupSummary(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + + notificationManager.notify(notificationId, builder.build()); + if (currentNotifications.length() == 1) { + notificationManager.notify((int) account.getId(), builder.setGroupSummary(true).build()); + } else { + notificationManager.notify((int) account.getId(), summaryBuilder.build()); + } + } + + private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) { + Intent summaryResultIntent = new Intent(context, MainActivity.class); + summaryResultIntent.putExtra(ACCOUNT_ID, account.getId()); + TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context); + summaryStackBuilder.addParentStack(MainActivity.class); + summaryStackBuilder.addNextIntent(summaryResultIntent); + + PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000), + PendingIntent.FLAG_UPDATE_CURRENT); + + // we have to switch account here + Intent eventResultIntent = new Intent(context, MainActivity.class); + eventResultIntent.putExtra(ACCOUNT_ID, account.getId()); + TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context); + eventStackBuilder.addParentStack(MainActivity.class); + eventStackBuilder.addNextIntent(eventResultIntent); + + PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(), + PendingIntent.FLAG_UPDATE_CURRENT); + + Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class); + deleteIntent.putExtra(ACCOUNT_ID, account.getId()); + PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body)) + .setSmallIcon(R.drawable.ic_notify) + .setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent) + .setDeleteIntent(deletePendingIntent) + .setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.tusky_blue)) + .setGroup(account.getAccountId()) + .setAutoCancel(true) + .setShortcutId(Long.toString(account.getId())) + .setDefaults(0); // So it doesn't ring twice, notify only in Target callback + + setupPreferences(account, builder); + + return builder; + } + + private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) { + Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) + .setAction(action) + .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) + .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) + .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) + .putExtra(KEY_NOTIFICATION_ID, notificationId); + + if(action == CHAT_REPLY_ACTION) { + replyIntent.putExtra(KEY_CHAT_ID, body.getChatMessage().getChatId()); + } else { + Status status = body.getStatus(); + + String citedLocalAuthor = status.getAccount().getLocalUsername(); + String citedText = status.getContent().toString(); + String inReplyToId = status.getId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + Status.Mention[] mentions = actionableStatus.getMentions(); + List mentionedUsernames = new ArrayList<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + for (Status.Mention mention : mentions) { + mentionedUsernames.add(mention.getUsername()); + } + mentionedUsernames.removeAll(Collections.singleton(account.getUsername())); + mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); + + replyIntent.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) + .putExtra(KEY_CITED_TEXT, citedText) + .putExtra(KEY_CITED_STATUS_ID, inReplyToId) + .putExtra(KEY_VISIBILITY, replyVisibility) + .putExtra(KEY_SPOILER, contentWarning) + .putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0])); + } + + return PendingIntent.getBroadcast(context.getApplicationContext(), + notificationId, + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { + if (NOTIFICATION_USE_CHANNELS) { + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + String[] channelIds = new String[]{ + CHANNEL_MENTION + account.getIdentifier(), + CHANNEL_FOLLOW + account.getIdentifier(), + CHANNEL_FOLLOW_REQUEST + account.getIdentifier(), + CHANNEL_BOOST + account.getIdentifier(), + CHANNEL_FAVOURITE + account.getIdentifier(), + CHANNEL_POLL + account.getIdentifier(), + CHANNEL_EMOJI_REACTION + account.getIdentifier(), + CHANNEL_CHAT_MESSAGES + account.getIdentifier(), + CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), + CHANNEL_MOVE + account.getIdentifier() + }; + int[] channelNames = { + R.string.notification_mention_name, + R.string.notification_follow_name, + R.string.notification_follow_request_name, + R.string.notification_boost_name, + R.string.notification_favourite_name, + R.string.notification_poll_name, + R.string.notification_emoji_name, + R.string.notification_chat_message_name, + R.string.notification_subscription_name, + R.string.notification_move_name + }; + int[] channelDescriptions = { + R.string.notification_mention_descriptions, + R.string.notification_follow_description, + R.string.notification_follow_request_description, + R.string.notification_boost_description, + R.string.notification_favourite_description, + R.string.notification_poll_description, + R.string.notification_emoji_description, + R.string.notification_chat_message_description, + R.string.notification_subscription_description, + R.string.notification_move_description + }; + + List channels = new ArrayList<>(9); + + NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); + + //noinspection ConstantConditions + notificationManager.createNotificationChannelGroup(channelGroup); + + for (int i = 0; i < channelIds.length; i++) { + String id = channelIds[i]; + String name = context.getString(channelNames[i]); + String description = context.getString(channelDescriptions[i]); + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(id, name, importance); + + channel.setDescription(description); + channel.enableLights(true); + channel.setLightColor(0xFF2B90D9); + channel.enableVibration(true); + channel.setShowBadge(true); + channel.setGroup(account.getIdentifier()); + channels.add(channel); + } + + notificationManager.createNotificationChannels(channels); + + } + } + + public static void deleteNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { + if (NOTIFICATION_USE_CHANNELS) { + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + //noinspection ConstantConditions + notificationManager.deleteNotificationChannelGroup(account.getIdentifier()); + + } + } + + public static void deleteLegacyNotificationChannels(@NonNull Context context, @NonNull AccountManager accountManager) { + if (NOTIFICATION_USE_CHANNELS) { + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // used until Tusky 1.4 + //noinspection ConstantConditions + notificationManager.deleteNotificationChannel(CHANNEL_MENTION); + notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE); + notificationManager.deleteNotificationChannel(CHANNEL_BOOST); + notificationManager.deleteNotificationChannel(CHANNEL_FOLLOW); + + // used until Tusky 1.7 + for(AccountEntity account: accountManager.getAllAccountsOrderedByActive()) { + notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE+" "+account.getIdentifier()); + } + } + } + + public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) { + if (NOTIFICATION_USE_CHANNELS) { + + // on Android >= O, notifications are enabled, if at least one channel is enabled + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + //noinspection ConstantConditions + if (notificationManager.areNotificationsEnabled()) { + for (NotificationChannel channel : notificationManager.getNotificationChannels()) { + if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { + Log.d(TAG, "NotificationsEnabled"); + return true; + } + } + } + Log.d(TAG, "NotificationsDisabled"); + + return false; + + } else { + // on Android < O, notifications are enabled, if at least one account has notification enabled + return accountManager.areNotificationsEnabled(); + } + + } + + public static void enablePullNotifications(Context context) { + WorkManager workManager = WorkManager.getInstance(context); + workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG); + + WorkRequest workRequest = new PeriodicWorkRequest.Builder( + NotificationWorker.class, + PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, + PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS + ) + .addTag(NOTIFICATION_PULL_TAG) + .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .build(); + + workManager.enqueue(workRequest); + + Log.d(TAG, "enabled notification checks with "+ PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS + "ms interval"); + } + + public static void disablePullNotifications(Context context) { + WorkManager.getInstance(context).cancelAllWorkByTag(NOTIFICATION_PULL_TAG); + Log.d(TAG, "disabled notification checks"); + } + + public static void clearNotificationsForActiveAccount(@NonNull Context context, @NonNull AccountManager accountManager) { + AccountEntity account = accountManager.getActiveAccount(); + if (account != null && !account.getActiveNotifications().equals("[]")) { + Single.fromCallable(() -> { + account.setActiveNotifications("[]"); + accountManager.saveAccount(account); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + //noinspection ConstantConditions + notificationManager.cancel((int) account.getId()); + return true; + }) + .subscribeOn(Schedulers.io()) + .subscribe(); + } + } + + private static boolean filterNotification(AccountEntity account, Notification notification, + Context context) { + + if (NOTIFICATION_USE_CHANNELS) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + String channelId = getChannelId(account, notification); + if(channelId == null) { + // unknown notificationtype + return false; + } + //noinspection ConstantConditions + NotificationChannel channel = notificationManager.getNotificationChannel(channelId); + return channel.getImportance() > NotificationManager.IMPORTANCE_NONE; + } + + switch (notification.getType()) { + case MENTION: + return account.getNotificationsMentioned(); + case STATUS: + return account.getNotificationsSubscriptions(); + case FOLLOW: + return account.getNotificationsFollowed(); + case FOLLOW_REQUEST: + return account.getNotificationsFollowRequested(); + case REBLOG: + return account.getNotificationsReblogged(); + case FAVOURITE: + return account.getNotificationsFavorited(); + case POLL: + return account.getNotificationsPolls(); + case EMOJI_REACTION: + return account.getNotificationsEmojiReactions(); + case CHAT_MESSAGE: + return account.getNotificationsChatMessages(); + case MOVE: + return account.getNotificationsMove(); + default: + return false; + } + } + + @Nullable + private static String getChannelId(AccountEntity account, Notification notification) { + switch (notification.getType()) { + case MENTION: + return CHANNEL_MENTION + account.getIdentifier(); + case STATUS: + return CHANNEL_SUBSCRIPTIONS + account.getIdentifier(); + case FOLLOW: + return CHANNEL_FOLLOW + account.getIdentifier(); + case FOLLOW_REQUEST: + return CHANNEL_FOLLOW_REQUEST + account.getIdentifier(); + case REBLOG: + return CHANNEL_BOOST + account.getIdentifier(); + case FAVOURITE: + return CHANNEL_FAVOURITE + account.getIdentifier(); + case POLL: + return CHANNEL_POLL + account.getIdentifier(); + case EMOJI_REACTION: + return CHANNEL_EMOJI_REACTION + account.getIdentifier(); + case CHAT_MESSAGE: + return CHANNEL_CHAT_MESSAGES + account.getIdentifier(); + case MOVE: + return CHANNEL_MOVE + account.getIdentifier(); + default: + return null; + } + + } + + private static void setupPreferences(AccountEntity account, + NotificationCompat.Builder builder) { + + if (NOTIFICATION_USE_CHANNELS) { + return; //do nothing on Android O or newer, the system uses the channel settings anyway + } + + if (account.getNotificationSound()) { + builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI); + } + + if (account.getNotificationVibration()) { + builder.setVibrate(new long[]{500, 500}); + } + + if (account.getNotificationLight()) { + builder.setLights(0xFF2B90D9, 300, 1000); + } + } + + private static String wrapItemAt(JSONArray array, int index) throws JSONException { + return StringUtils.unicodeWrap(array.get(index).toString()); + } + + @Nullable + private static String joinNames(Context context, JSONArray array) throws JSONException { + if (array.length() > 3) { + int length = array.length(); + return String.format(context.getString(R.string.notification_summary_large), + wrapItemAt(array, length - 1), + wrapItemAt(array, length - 2), + wrapItemAt(array, length - 3), + length - 3); + } else if (array.length() == 3) { + return String.format(context.getString(R.string.notification_summary_medium), + wrapItemAt(array, 2), + wrapItemAt(array, 1), + wrapItemAt(array, 0)); + } else if (array.length() == 2) { + return String.format(context.getString(R.string.notification_summary_small), + wrapItemAt(array, 1), + wrapItemAt(array, 0)); + } + + return null; + } + + @Nullable + private static String titleForType(Context context, Notification notification, AccountEntity account) { + String accountName = StringUtils.unicodeWrap(notification.getAccount().getName()); + switch (notification.getType()) { + case MENTION: + return String.format(context.getString(R.string.notification_mention_format), + accountName); + case STATUS: + return String.format(context.getString(R.string.notification_subscription_format), + accountName); + case FOLLOW: + return String.format(context.getString(R.string.notification_follow_format), + accountName); + case FOLLOW_REQUEST: + return String.format(context.getString(R.string.notification_follow_request_format), + accountName); + case FAVOURITE: + return String.format(context.getString(R.string.notification_favourite_format), + accountName); + case REBLOG: + return String.format(context.getString(R.string.notification_reblog_format), + accountName); + case EMOJI_REACTION: + return String.format(context.getString(R.string.notification_emoji_format), + accountName, notification.getEmoji()); + case POLL: + if(notification.getStatus().getAccount().getId().equals(account.getAccountId())) { + return context.getString(R.string.poll_ended_created); + } else { + return context.getString(R.string.poll_ended_voted); + } + case CHAT_MESSAGE: + return String.format(context.getString(R.string.notification_chat_message_format), + accountName); + case MOVE: { + return String.format(context.getString(R.string.notification_move_format), accountName); + } + } + return null; + } + + private static String bodyForType(Notification notification, Context context) { + switch (notification.getType()) { + case MOVE: + return "@" + notification.getTarget().getUsername(); + case FOLLOW: + case FOLLOW_REQUEST: + return "@" + notification.getAccount().getUsername(); + case MENTION: + case FAVOURITE: + case REBLOG: + case EMOJI_REACTION: + case STATUS: + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { + return notification.getStatus().getSpoilerText(); + } else { + return notification.getStatus().getContent().toString(); + } + case POLL: + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { + return notification.getStatus().getSpoilerText(); + } else { + StringBuilder builder = new StringBuilder(notification.getStatus().getContent()); + builder.append('\n'); + Poll poll = notification.getStatus().getPoll(); + for(PollOption option: poll.getOptions()) { + builder.append(buildDescription(option.getTitle(), + PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()), + context)); + builder.append('\n'); + } + return builder.toString(); + } + case CHAT_MESSAGE: + if (!TextUtils.isEmpty(notification.getChatMessage().getContent())) { + return notification.getChatMessage().getContent().toString(); + } else if(notification.getChatMessage().getAttachment() != null) { + return context.getString(notification.getChatMessage().getAttachment().describeAttachmentType()); + } else if(notification.getChatMessage().getCard() != null) { + return context.getString(R.string.link); + } else { + return ""; + } + } + return null; + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt new file mode 100644 index 0000000..ae7d4d3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt @@ -0,0 +1,51 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Tusky. If + * not, see . */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.Worker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import javax.inject.Inject + +class NotificationWorker( + context: Context, + params: WorkerParameters, + private val notificationsFetcher: NotificationFetcher +) : Worker(context, params) { + + override fun doWork(): Result { + notificationsFetcher.fetchAndShow() + return Result.success() + } +} + +class NotificationWorkerFactory @Inject constructor( + private val notificationsFetcher: NotificationFetcher +) : WorkerFactory() { + + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + if (workerClassName == NotificationWorker::class.java.name) { + return NotificationWorker(appContext, workerParameters, notificationsFetcher) + } + return null + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt new file mode 100644 index 0000000..35c33a9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt @@ -0,0 +1,20 @@ +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.entity.Notification + +/** + * Shows notifications. + */ +interface Notifier { + fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) +} + +class SystemNotifier( + private val context: Context +) : Notifier { + override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) { + NotificationHelper.make(context, notification, account, isFirstInBatch) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt new file mode 100644 index 0000000..5001472 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -0,0 +1,390 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.annotation.DrawableRes +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.instancemute.InstanceListActivity +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.* +import com.keylesspalace.tusky.util.ThemeUtils +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeRes +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import javax.inject.Inject + +class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val context = requireContext() + makePreferenceScreen { + preference { + setTitle(R.string.pref_title_edit_notification_settings) + icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply { + sizeRes = R.dimen.preference_icon_size + colorInt = ThemeUtils.getColor(context, R.attr.iconColor) + } + setOnPreferenceClickListener { + openNotificationPrefs() + true + } + } + + preference { + setTitle(R.string.title_tab_preferences) + setIcon(R.drawable.ic_tabs) + setOnPreferenceClickListener { + val intent = Intent(context, TabPreferenceActivity::class.java) + activity?.startActivity(intent) + activity?.overridePendingTransition(R.anim.slide_from_right, + R.anim.slide_to_left) + true + } + } + + preference { + setTitle(R.string.action_view_mutes) + setIcon(R.drawable.ic_mute_24dp) + setOnPreferenceClickListener { + val intent = Intent(context, AccountListActivity::class.java) + intent.putExtra("type", AccountListActivity.Type.MUTES) + activity?.startActivity(intent) + activity?.overridePendingTransition(R.anim.slide_from_right, + R.anim.slide_to_left) + true + } + } + + preference { + setTitle(R.string.action_view_blocks) + icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply { + sizeRes = R.dimen.preference_icon_size + colorInt = ThemeUtils.getColor(context, R.attr.iconColor) + } + setOnPreferenceClickListener { + val intent = Intent(context, AccountListActivity::class.java) + intent.putExtra("type", AccountListActivity.Type.BLOCKS) + activity?.startActivity(intent) + activity?.overridePendingTransition(R.anim.slide_from_right, + R.anim.slide_to_left) + true + } + } + + preference { + setTitle(R.string.title_domain_mutes) + setIcon(R.drawable.ic_mute_24dp) + setOnPreferenceClickListener { + val intent = Intent(context, InstanceListActivity::class.java) + activity?.startActivity(intent) + activity?.overridePendingTransition(R.anim.slide_from_right, + R.anim.slide_to_left) + true + } + } + + preferenceCategory(R.string.pref_publishing) { + listPreference { + setTitle(R.string.pref_default_post_privacy) + setEntries(R.array.post_privacy_names) + setEntryValues(R.array.post_privacy_values) + key = PrefKeys.DEFAULT_POST_PRIVACY + setSummaryProvider { entry } + val visibility = accountManager.activeAccount?.defaultPostPrivacy + ?: Status.Visibility.PUBLIC + value = visibility.serverString() + setIcon(getIconForVisibility(visibility)) + setOnPreferenceChangeListener { _, newValue -> + setIcon(getIconForVisibility(Status.Visibility.byString(newValue as String))) + syncWithServer(visibility = newValue) + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + + listPreference { + setTitle(R.string.pref_title_default_formatting) + setEntries(R.array.formatting_syntax_values) + setEntryValues(R.array.formatting_syntax_values) + key = PrefKeys.DEFAULT_FORMATTING_SYNTAX + setSummaryProvider { entry } + val syntax = accountManager.activeAccount?.defaultFormattingSyntax + ?: "" + value = when(syntax) { + "text/markdown" -> "Markdown" + "text/bbcode" -> "BBCode" + "text/html" -> "HTML" + else -> "Plaintext" + } + setIcon(getIconForSyntax(syntax)) + setOnPreferenceChangeListener { _, newValue -> + val syntax = when(newValue) { + "Markdown" -> "text/markdown" + "BBCode" -> "text/bbcode" + "HTML" -> "text/html" + else -> "" + } + setIcon(getIconForSyntax(syntax)) + updateAccount { it.defaultFormattingSyntax = syntax } + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + + } + + switchPreference { + setTitle(R.string.pref_default_media_sensitivity) + setIcon(R.drawable.ic_eye_24dp) + key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY + isSingleLineTitle = false + val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity + ?: false + setDefaultValue(sensitivity) + setIcon(getIconForSensitivity(sensitivity)) + setOnPreferenceChangeListener { _, newValue -> + setIcon(getIconForSensitivity(newValue as Boolean)) + syncWithServer(sensitive = newValue) + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + } + + preferenceCategory(R.string.pref_title_timelines) { + switchPreference { + key = PrefKeys.MEDIA_PREVIEW_ENABLED + setTitle(R.string.pref_title_show_media_preview) + isSingleLineTitle = false + isChecked = accountManager.activeAccount?.mediaPreviewEnabled ?: true + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.mediaPreviewEnabled = newValue as Boolean } + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + + switchPreference { + key = PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA + setTitle(R.string.pref_title_alway_show_sensitive_media) + isSingleLineTitle = false + isChecked = accountManager.activeAccount?.alwaysShowSensitiveMedia ?: false + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.alwaysShowSensitiveMedia = newValue as Boolean } + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + + switchPreference { + key = PrefKeys.ALWAYS_OPEN_SPOILER + setTitle(R.string.pref_title_alway_open_spoiler) + isSingleLineTitle = false + isChecked = accountManager.activeAccount?.alwaysOpenSpoiler ?: false + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.alwaysOpenSpoiler = newValue as Boolean } + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + } + + preferenceCategory(R.string.pref_title_other) { + switchPreference { + key = PrefKeys.LIVE_NOTIFICATIONS + setTitle(R.string.pref_title_live_notifications) + setSummary(R.string.pref_summary_live_notifications) + isSingleLineTitle = false + isChecked = accountManager.activeAccount?.notificationsStreamingEnabled ?: false + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsStreamingEnabled = newValue as Boolean } + eventHub.dispatch(PreferenceChangedEvent(key)) + true + } + } + } + + preferenceCategory(R.string.pref_title_timeline_filters) { + preference { + setTitle(R.string.pref_title_public_filter_keywords) + setOnPreferenceClickListener { + launchFilterActivity(Filter.PUBLIC, + R.string.pref_title_public_filter_keywords) + true + } + } + + preference { + setTitle(R.string.title_notifications) + setOnPreferenceClickListener { + launchFilterActivity(Filter.NOTIFICATIONS, R.string.title_notifications) + true + } + } + + preference { + setTitle(R.string.title_home) + setOnPreferenceClickListener { + launchFilterActivity(Filter.HOME, R.string.title_home) + true + } + } + + preference { + setTitle(R.string.pref_title_thread_filter_keywords) + setOnPreferenceClickListener { + launchFilterActivity(Filter.THREAD, + R.string.pref_title_thread_filter_keywords) + true + } + } + + preference { + setTitle(R.string.title_accounts) + setOnPreferenceClickListener { + launchFilterActivity(Filter.ACCOUNT, R.string.title_accounts) + true + } + } + } + } + } + + private fun openNotificationPrefs() { + if (NotificationHelper.NOTIFICATION_USE_CHANNELS) { + val intent = Intent() + intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" + intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID) + startActivity(intent) + } else { + activity?.let { + val intent = PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES) + it.startActivity(intent) + it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } + + } + } + + private inline fun updateAccount(changer: (AccountEntity) -> Unit) { + accountManager.activeAccount?.let { account -> + changer(account) + accountManager.saveAccount(account) + } + } + + private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) { + mastodonApi.accountUpdateSource(visibility, sensitive) + .enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val account = response.body() + if (response.isSuccessful && account != null) { + + accountManager.activeAccount?.let { + it.defaultPostPrivacy = account.source?.privacy + ?: Status.Visibility.PUBLIC + it.defaultMediaSensitivity = account.source?.sensitive ?: false + accountManager.saveAccount(it) + } + } else { + Log.e("AccountPreferences", "failed updating settings on server") + showErrorSnackbar(visibility, sensitive) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e("AccountPreferences", "failed updating settings on server", t) + showErrorSnackbar(visibility, sensitive) + } + + }) + } + + private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) { + view?.let { view -> + Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) } + .show() + } + } + + @DrawableRes + private fun getIconForVisibility(visibility: Status.Visibility): Int { + return when (visibility) { + Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp + + Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp + + else -> R.drawable.ic_public_24dp + } + } + + @DrawableRes + private fun getIconForSensitivity(sensitive: Boolean): Int { + return if (sensitive) { + R.drawable.ic_hide_media_24dp + } else { + R.drawable.ic_eye_24dp + } + } + + private fun getIconForSyntax(syntax: String): Int { + return when(syntax) { + "text/html" -> R.drawable.ic_html_24dp + "text/bbcode" -> R.drawable.ic_bbcode_24dp + "text/markdown" -> R.drawable.ic_markdown + else -> android.R.color.transparent + } + } + + private fun launchFilterActivity(filterContext: String, titleResource: Int) { + val intent = Intent(context, FiltersActivity::class.java) + intent.putExtra(FiltersActivity.FILTERS_CONTEXT, filterContext) + intent.putExtra(FiltersActivity.FILTERS_TITLE, getString(titleResource)) + activity?.startActivity(intent) + activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } + + companion object { + fun newInstance() = AccountPreferencesFragment() + } +} 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 new file mode 100644 index 0000000..c0e538a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -0,0 +1,258 @@ +package com.keylesspalace.tusky.components.preference + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.* +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.util.EmojiCompatFont +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import okhttp3.OkHttpClient +import kotlin.system.exitProcess + +/** + * This Preference lets the user select their preferred emoji font + */ +class EmojiPreference( + context: Context, + private val okHttpClient: OkHttpClient +) : Preference(context) { + + private lateinit var selected: EmojiCompatFont + private lateinit var original: EmojiCompatFont + private val radioButtons = mutableListOf() + private var updated = false + private var currentNeedsUpdate = false + + private val downloadDisposables = MutableList(FONTS.size) { null } + + override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) { + super.onAttachedToHierarchy(preferenceManager) + + // Find out which font is currently active + selected = EmojiCompatFont.byId( + PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) + ) + // We'll use this later to determine if anything has changed + original = selected + summary = selected.getDisplay(context) + } + + override fun onClick() { + val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) + viewIds.forEachIndexed { index, viewId -> + setupItem(view.findViewById(viewId), FONTS[index]) + } + AlertDialog.Builder(context) + .setView(view) + .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) + + // Initialize all the views + title.text = font.getDisplay(container.context) + caption.setText(font.caption) + thumb.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) + + // 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)) + } + } + + 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) + + // Switch to downloading style + download.visibility = View.GONE + caption.visibility = View.INVISIBLE + progressBar.visibility = View.VISIBLE + progressBar.progress = 0 + cancel.visibility = View.VISIBLE + 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() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + progressBar.setProgress((max * progress).toInt(), true) + } else { + progressBar.progress = (max * progress).toInt() + } + } else { + progressBar.isIndeterminate = true + } + }, + { + Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() + updateItem(font, container) + }, + { + finishDownload(font, container) + } + ).also { downloadDisposables[font.id] = it } + + + } + + private fun cancelDownload(font: EmojiCompatFont, container: View) { + font.deleteDownloadedFile(container.context) + downloadDisposables[font.id]?.dispose() + downloadDisposables[font.id] = null + updateItem(font, container) + } + + private fun finishDownload(font: EmojiCompatFont, container: View) { + select(font, container.findViewById(R.id.emojicompat_radio)) + updateItem(font, container) + // Set the flag to restart the app (because an update has been downloaded) + if (selected === original && currentNeedsUpdate) { + updated = true + currentNeedsUpdate = false + } + } + + /** + * Select a font both visually and logically + * + * @param font The font to be selected + * @param radio The radio button associated with it's visual item + */ + private fun select(font: EmojiCompatFont, radio: RadioButton) { + selected = font + // Uncheck all the other buttons + for (other in radioButtons) { + if (other !== radio) { + other.isChecked = false + } + } + 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 + */ + 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) + + // There's no download going on + progress.visibility = View.GONE + cancel.visibility = View.GONE + caption.visibility = View.VISIBLE + if (font.isDownloaded(context)) { + // Make it selectable + download.visibility = View.GONE + radio.visibility = View.VISIBLE + container.isClickable = true + } else { + // Make it downloadable + download.visibility = View.VISIBLE + radio.visibility = View.GONE + container.isClickable = false + } + + // Select it if necessary + if (font === selected) { + radio.isChecked = true + // Update available + if (!font.isDownloaded(context)) { + currentNeedsUpdate = true + } + } else { + radio.isChecked = false + } + } + + private fun saveSelectedFont() { + val index = selected.id + Log.i(TAG, "saveSelectedFont: Font ID: $index") + PreferenceManager + .getDefaultSharedPreferences(context) + .edit() + .putInt(key, index) + .apply() + summary = selected.getDisplay(context) + } + + /** + * User clicked ok -> save the selected font and offer to restart the app if something changed + */ + private fun onDialogOk() { + saveSelectedFont() + if (selected !== original || updated) { + AlertDialog.Builder(context) + .setTitle(R.string.restart_required) + .setMessage(R.string.restart_emoji) + .setNegativeButton(R.string.later, null) + .setPositiveButton(R.string.restart) { _, _ -> + // Restart the app + // From https://stackoverflow.com/a/17166729/5070653 + val launchIntent = Intent(context, SplashActivity::class.java) + val mPendingIntent = PendingIntent.getActivity( + context, + 0x1f973, // This is the codepoint of the party face emoji :D + launchIntent, + PendingIntent.FLAG_CANCEL_CURRENT) + val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + mgr.set( + AlarmManager.RTC, + System.currentTimeMillis() + 100, + mPendingIntent) + exitProcess(0) + }.show() + } + } + + 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/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt new file mode 100644 index 0000000..935c47e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -0,0 +1,213 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference +import javax.inject.Inject + +class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { + + @Inject + lateinit var accountManager: AccountManager + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val activeAccount = accountManager.activeAccount ?: return + val context = requireContext() + makePreferenceScreen { + switchPreference { + setTitle(R.string.pref_title_notifications_enabled) + key = PrefKeys.NOTIFICATIONS_ENABLED + isIconSpaceReserved = false + isChecked = activeAccount.notificationsEnabled + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsEnabled = newValue as Boolean } + if (NotificationHelper.areNotificationsEnabled(context, accountManager)) { + NotificationHelper.enablePullNotifications(context) + } else { + NotificationHelper.disablePullNotifications(context) + } + true + } + } + + preferenceCategory(R.string.pref_title_notification_filters) { category -> + category.dependency = PrefKeys.NOTIFICATIONS_ENABLED + category.isIconSpaceReserved = false + + switchPreference { + setTitle(R.string.pref_title_notification_filter_follows) + key = PrefKeys.NOTIFICATIONS_FILTER_FOLLOWS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFollowed + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFollowed = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_follow_requests) + key = PrefKeys.NOTIFICATION_FILTER_FOLLOW_REQUESTS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFollowRequested + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFollowRequested = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_reblogs) + key = PrefKeys.NOTIFICATION_FILTER_REBLOGS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsReblogged + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsReblogged = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_favourites) + key = PrefKeys.NOTIFICATION_FILTER_FAVS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFavorited + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFavorited = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_emoji) + key = PrefKeys.NOTIFICATION_FILTER_EMOJI_REACTIONS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsEmojiReactions + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsEmojiReactions = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_poll) + key = PrefKeys.NOTIFICATION_FILTER_POLLS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsPolls + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsPolls = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_chat_messages) + key = PrefKeys.NOTIFICATION_FILTER_CHAT_MESSAGES + isIconSpaceReserved = false + isChecked = activeAccount.notificationsChatMessages + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsChatMessages = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_subscriptions) + key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSubscriptions + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSubscriptions = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_move) + key = PrefKeys.NOTIFICATION_FILTER_MOVE + isIconSpaceReserved = false + isChecked = activeAccount.notificationsMove + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsMove = newValue as Boolean } + true + } + } + } + + preferenceCategory(R.string.pref_title_notification_alerts) { category -> + category.dependency = PrefKeys.NOTIFICATIONS_ENABLED + category.isIconSpaceReserved = false + + switchPreference { + setTitle(R.string.pref_title_notification_alert_sound) + key = PrefKeys.NOTIFICATION_ALERT_SOUND + isIconSpaceReserved = false + isChecked = activeAccount.notificationSound + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationSound = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_alert_vibrate) + key = PrefKeys.NOTIFICATION_ALERT_VIBRATE + isIconSpaceReserved = false + isChecked = activeAccount.notificationVibration + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationVibration = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_alert_light) + key = PrefKeys.NOTIFICATION_ALERT_LIGHT + isIconSpaceReserved = false + isChecked = activeAccount.notificationLight + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationLight = newValue as Boolean } + true + } + } + } + } + } + + private inline fun updateAccount(changer: (AccountEntity) -> Unit) { + accountManager.activeAccount?.let { account -> + changer(account) + accountManager.saveAccount(account) + } + } + + companion object { + fun newInstance(): NotificationPreferencesFragment { + return NotificationPreferencesFragment() + } + } + +} 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 new file mode 100644 index 0000000..8618711 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -0,0 +1,192 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import androidx.fragment.app.Fragment +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.BaseActivity +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.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, + HasAndroidInjector { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + private var restartActivitiesOnExit: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_preferences) + + setSupportActionBar(toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { + GENERAL_PREFERENCES -> { + setTitle(R.string.action_view_preferences) + PreferencesFragment.newInstance() + } + ACCOUNT_PREFERENCES -> { + setTitle(R.string.action_view_account_preferences) + AccountPreferencesFragment.newInstance() + } + NOTIFICATION_PREFERENCES -> { + setTitle(R.string.pref_title_edit_notification_settings) + NotificationPreferencesFragment.newInstance() + } + TAB_FILTER_PREFERENCES -> { + setTitle(R.string.pref_title_status_tabs) + TabFilterPreferencesFragment.newInstance() + } + PROXY_PREFERENCES -> { + setTitle(R.string.pref_title_http_proxy_settings) + ProxyPreferencesFragment.newInstance() + } + else -> throw IllegalArgumentException("preferenceType not known") + } + + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit() + + restartActivitiesOnExit = intent.getBooleanExtra("restart", false) + + } + + override fun onResume() { + super.onResume() + PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun saveInstanceState(outState: Bundle) { + outState.putBoolean("restart", restartActivitiesOnExit) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean("restart", restartActivitiesOnExit) + super.onSaveInstanceState(outState) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + when (key) { + "appTheme" -> { + val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT) + Log.d("activeTheme", theme) + ThemeUtils.setAppNightMode(theme) + + restartActivitiesOnExit = true + this.restartCurrentActivity() + + } + "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", + "useBlurhash", "showCardsInTimelines", "confirmReblogs", + "enableSwipeForTabs", "bigEmojis", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, + PrefKeys.RENDER_STATUS_AS_MENTION -> { + restartActivitiesOnExit = true + } + "language" -> { + restartActivitiesOnExit = true + this.restartCurrentActivity() + } + } + + eventHub.dispatch(PreferenceChangedEvent(key)) + } + + private fun restartCurrentActivity() { + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val savedInstanceState = Bundle() + saveInstanceState(savedInstanceState) + intent.putExtras(savedInstanceState) + startActivityWithSlideInAnimation(intent) + finish() + overridePendingTransition(R.anim.fade_in, R.anim.fade_out) + } + + override fun onBackPressed() { + /* Switching themes won't actually change the theme of activities on the back stack. + * Either the back stack activities need to all be recreated, or do the easier thing, which + * is hijack the back button press and use it to launch a new MainActivity and clear the + * back stack. */ + if (restartActivitiesOnExit) { + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivityWithSlideInAnimation(intent) + } else { + super.onBackPressed() + } + } + + override fun androidInjector() = androidInjector + + companion object { + + const val GENERAL_PREFERENCES = 0 + const val ACCOUNT_PREFERENCES = 1 + const val NOTIFICATION_PREFERENCES = 2 + const val TAB_FILTER_PREFERENCES = 3 + const val PROXY_PREFERENCES = 4 + private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE" + + @JvmStatic + fun newIntent(context: Context, preferenceType: Int): Intent { + val intent = Intent(context, PreferencesActivity::class.java) + intent.putExtra(EXTRA_PREFERENCE_TYPE, preferenceType) + return intent + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt new file mode 100644 index 0000000..8b3f151 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -0,0 +1,340 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.settings.* +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.deserialize +import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.serialize +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizePx +import okhttp3.OkHttpClient +import javax.inject.Inject + +class PreferencesFragment : PreferenceFragmentCompat(), Injectable { + + @Inject + lateinit var okhttpclient: OkHttpClient + + @Inject + lateinit var accountManager: AccountManager + + private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } + private var httpProxyPref: Preference? = null + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + preferenceCategory(R.string.pref_title_appearance_settings) { + listPreference { + setDefaultValue(AppTheme.NIGHT.value) + setEntries(R.array.app_theme_names) + entryValues = AppTheme.stringValues() + key = PrefKeys.APP_THEME + setSummaryProvider { entry } + setTitle(R.string.pref_title_app_theme) + icon = makeIcon(GoogleMaterial.Icon.gmd_palette) + } + + emojiPreference(okhttpclient) { + setDefaultValue("system_default") + setIcon(R.drawable.ic_emoji_24dp) + key = PrefKeys.EMOJI + setSummary(R.string.system_default) + setTitle(R.string.emoji_style) + icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied) + } + + listPreference { + setDefaultValue("default") + setEntries(R.array.language_entries) + setEntryValues(R.array.language_values) + key = PrefKeys.LANGUAGE + setSummaryProvider { entry } + setTitle(R.string.pref_title_language) + icon = makeIcon(GoogleMaterial.Icon.gmd_translate) + } + + listPreference { + setDefaultValue("medium") + setEntries(R.array.status_text_size_names) + setEntryValues(R.array.status_text_size_values) + key = PrefKeys.STATUS_TEXT_SIZE + setSummaryProvider { entry } + setTitle(R.string.pref_status_text_size) + icon = makeIcon(GoogleMaterial.Icon.gmd_format_size) + } + + listPreference { + setDefaultValue("top") + setEntries(R.array.pref_main_nav_position_options) + setEntryValues(R.array.pref_main_nav_position_values) + key = PrefKeys.MAIN_NAV_POSITION + setSummaryProvider { entry } + setTitle(R.string.pref_main_nav_position) + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.HIDE_TOP_TOOLBAR + setTitle(R.string.pref_title_hide_top_toolbar) + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.FAB_HIDE + setTitle(R.string.pref_title_hide_follow_button) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ABSOLUTE_TIME_VIEW + setTitle(R.string.pref_title_absolute_time) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.SHOW_BOT_OVERLAY + setTitle(R.string.pref_title_bot_overlay) + isSingleLineTitle = false + setIcon(R.drawable.ic_bot_24dp) + + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_GIF_AVATARS + setTitle(R.string.pref_title_animate_gif_avatars) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.USE_BLURHASH + setTitle(R.string.pref_title_gradient_for_media) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.SHOW_CARDS_IN_TIMELINES + setTitle(R.string.pref_title_show_cards_in_timelines) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.SHOW_NOTIFICATIONS_FILTER + setTitle(R.string.pref_title_show_notifications_filter) + isSingleLineTitle = false + setOnPreferenceClickListener { + activity?.let { activity -> + val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES) + activity.startActivity(intent) + activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } + true + } + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.CONFIRM_REBLOGS + setTitle(R.string.pref_title_confirm_reblogs) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.HIDE_MUTED_USERS + setTitle(R.string.pref_title_hide_muted_users) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.ENABLE_SWIPE_FOR_TABS + setTitle(R.string.pref_title_enable_swipe_for_tabs) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.BIG_EMOJIS + setTitle(R.string.pref_title_enable_big_emojis) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.STICKERS + setTitle(R.string.pref_title_enable_experimental_stickers) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_CUSTOM_EMOJIS + setTitle(R.string.pref_title_animate_custom_emojis) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.RENDER_STATUS_AS_MENTION + setTitle(R.string.pref_title_render_subscriptions_as_statuses) + isSingleLineTitle = true + } + } + + preferenceCategory(R.string.pref_title_privacy) { + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANONYMIZE_FILENAMES + setTitle(R.string.pref_title_anonymize_upload_filenames) + isSingleLineTitle = false + } + } + + preferenceCategory(R.string.pref_title_browser_settings) { + switchPreference { + setDefaultValue(false) + key = PrefKeys.CUSTOM_TABS + setTitle(R.string.pref_title_custom_tabs) + isSingleLineTitle = false + } + } + + preferenceCategory(R.string.pref_title_timeline_filters) { + preference { + setTitle(R.string.pref_title_status_tabs) + setOnPreferenceClickListener { + activity?.let { activity -> + val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES) + activity.startActivity(intent) + activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } + true + } + } + } + + preferenceCategory(R.string.pref_title_wellbeing_mode) { + switchPreference { + title = getString(R.string.limit_notifications) + setDefaultValue(false) + key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS + setOnPreferenceChangeListener { _, value -> + for (account in accountManager.accounts) { + val notificationFilter = deserialize(account.notificationsFilter).toMutableSet() + + if (value == true) { + notificationFilter.add(Notification.Type.FAVOURITE) + notificationFilter.add(Notification.Type.FOLLOW) + notificationFilter.add(Notification.Type.REBLOG) + } else { + notificationFilter.remove(Notification.Type.FAVOURITE) + notificationFilter.remove(Notification.Type.FOLLOW) + notificationFilter.remove(Notification.Type.REBLOG) + } + + account.notificationsFilter = serialize(notificationFilter) + accountManager.saveAccount(account) + } + true + } + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_posts) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_POSTS + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_profile) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_PROFILE + } + } + + preferenceCategory(R.string.pref_title_proxy_settings) { + httpProxyPref = preference { + setTitle(R.string.pref_title_http_proxy_settings) + setOnPreferenceClickListener { + activity?.let { activity -> + val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.PROXY_PREFERENCES) + activity.startActivity(intent) + activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } + true + } + } + } + } + } + + private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable { + val context = requireContext() + return IconicsDrawable(context, icon).apply { + sizePx = iconSize + colorInt = ThemeUtils.getColor(context, R.attr.iconColor) + } + + } + + override fun onResume() { + super.onResume() + updateHttpProxySummary() + } + + private fun updateHttpProxySummary() { + val sharedPreferences = preferenceManager.sharedPreferences + val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false) + val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "") + + try { + val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1") + .toInt() + + if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) { + httpProxyPref?.summary = "$httpServer:$httpPort" + return + } + } catch (e: NumberFormatException) { + // user has entered wrong port, fall back to empty summary + } + + httpProxyPref?.summary = "" + } + + companion object { + fun newInstance(): PreferencesFragment { + return PreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt new file mode 100644 index 0000000..922d5a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt @@ -0,0 +1,69 @@ +/* Copyright 2018 Conny Duck + + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.editTextPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.switchPreference +import kotlin.system.exitProcess + +class ProxyPreferencesFragment : PreferenceFragmentCompat() { + private var pendingRestart = false + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + switchPreference { + setTitle(R.string.pref_title_http_proxy_enable) + isIconSpaceReserved = false + key = PrefKeys.HTTP_PROXY_ENABLED + setDefaultValue(false) + } + + editTextPreference { + setTitle(R.string.pref_title_http_proxy_server) + key = PrefKeys.HTTP_PROXY_SERVER + isIconSpaceReserved = false + setSummaryProvider { text } + } + + editTextPreference { + setTitle(R.string.pref_title_http_proxy_port) + key = PrefKeys.HTTP_PROXY_PORT + isIconSpaceReserved = false + setSummaryProvider { text } + } + } + + } + + override fun onPause() { + super.onPause() + if (pendingRestart) { + pendingRestart = false + exitProcess(0) + } + } + + companion object { + fun newInstance(): ProxyPreferencesFragment { + return ProxyPreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt new file mode 100644 index 0000000..71c5e10 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt @@ -0,0 +1,54 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.checkBoxPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory + +class TabFilterPreferencesFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + preferenceCategory(R.string.title_home) { category -> + category.isIconSpaceReserved = false + + checkBoxPreference { + setTitle(R.string.pref_title_show_boosts) + key = PrefKeys.TAB_FILTER_HOME_BOOSTS + setDefaultValue(true) + isIconSpaceReserved = false + } + + checkBoxPreference { + setTitle(R.string.pref_title_show_replies) + key = PrefKeys.TAB_FILTER_HOME_REPLIES + setDefaultValue(false) + isIconSpaceReserved = false + } + } + } + } + + companion object { + fun newInstance(): TabFilterPreferencesFragment { + return TabFilterPreferencesFragment() + } + } +} 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 new file mode 100644 index 0000000..5daf584 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -0,0 +1,150 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.lifecycle.Observer +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter +import com.keylesspalace.tusky.di.ViewModelFactory +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 { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ReportViewModel by viewModels { viewModelFactory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val accountId = intent?.getStringExtra(ACCOUNT_ID) + val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) + if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { + throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null") + } + + viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) + + + setContentView(R.layout.activity_report) + + setSupportActionBar(toolbar) + + supportActionBar?.apply { + title = getString(R.string.report_username_format, viewModel.accountUserName) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_close_24dp) + } + + initViewPager() + if (savedInstanceState == null) { + viewModel.navigateTo(Screen.Statuses) + } + subscribeObservables() + } + + private fun initViewPager() { + wizard.isUserInputEnabled = false + wizard.adapter = ReportPagerAdapter(this) + } + + private fun subscribeObservables() { + viewModel.navigation.observe(this, Observer { screen -> + if (screen != null) { + viewModel.navigated() + when (screen) { + Screen.Statuses -> showStatusesPage() + Screen.Note -> showNotesPage() + Screen.Done -> showDonePage() + Screen.Back -> showPreviousScreen() + Screen.Finish -> closeScreen() + } + } + }) + + viewModel.checkUrl.observe(this, Observer { + if (!it.isNullOrBlank()) { + viewModel.urlChecked() + viewUrl(it) + } + }) + } + + private fun showPreviousScreen() { + when (wizard.currentItem) { + 0 -> closeScreen() + 1 -> showStatusesPage() + } + } + + private fun showDonePage() { + wizard.currentItem = 2 + } + + private fun showNotesPage() { + wizard.currentItem = 1 + } + + private fun closeScreen() { + finish() + } + + private fun showStatusesPage() { + wizard.currentItem = 0 + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + closeScreen() + return true + } + } + return super.onOptionsItemSelected(item) + } + + companion object { + private const val ACCOUNT_ID = "account_id" + private const val ACCOUNT_USERNAME = "account_username" + private const val STATUS_ID = "status_id" + + @JvmStatic + fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) = + Intent(context, ReportActivity::class.java) + .apply { + putExtra(ACCOUNT_ID, accountId) + putExtra(ACCOUNT_USERNAME, userName) + putExtra(STATUS_ID, statusId) + } + } + + override fun androidInjector() = androidInjector +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt new file mode 100644 index 0000000..8bb2e88 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -0,0 +1,225 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.paging.PagedList +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.components.report.adapter.StatusesRepository +import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class ReportViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub, + private val statusesRepository: StatusesRepository) : RxAwareViewModel() { + + private val navigationMutable = MutableLiveData() + val navigation: LiveData = navigationMutable + + private val muteStateMutable = MutableLiveData>() + val muteState: LiveData> = muteStateMutable + + private val blockStateMutable = MutableLiveData>() + val blockState: LiveData> = blockStateMutable + + private val reportingStateMutable = MutableLiveData>() + var reportingState: LiveData> = reportingStateMutable + + private val checkUrlMutable = MutableLiveData() + val checkUrl: LiveData = checkUrlMutable + + private val repoResult = MutableLiveData>() + val statuses: LiveData> = Transformations.switchMap(repoResult) { it.pagedList } + val networkStateAfter: LiveData = Transformations.switchMap(repoResult) { it.networkStateAfter } + val networkStateBefore: LiveData = Transformations.switchMap(repoResult) { it.networkStateBefore } + val networkStateRefresh: LiveData = Transformations.switchMap(repoResult) { it.refreshState } + + private val selectedIds = HashSet() + val statusViewState = StatusViewState() + + var reportNote: String = "" + var isRemoteNotify = false + + private var statusId: String? = null + lateinit var accountUserName: String + lateinit var accountId: String + var isRemoteAccount: Boolean = false + var remoteServer: String? = null + + fun init(accountId: String, userName: String, statusId: String?) { + this.accountId = accountId + this.accountUserName = userName + this.statusId = statusId + statusId?.let { + selectedIds.add(it) + } + + isRemoteAccount = userName.contains('@') + if (isRemoteAccount) { + remoteServer = userName.substring(userName.indexOf('@') + 1) + } + + obtainRelationship() + repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables) + } + + fun navigateTo(screen: Screen) { + navigationMutable.value = screen + } + + fun navigated() { + navigationMutable.value = null + } + + + private fun obtainRelationship() { + val ids = listOf(accountId) + muteStateMutable.value = Loading() + blockStateMutable.value = Loading() + mastodonApi.relationships(ids) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { data -> + updateRelationship(data.getOrNull(0)) + + }, + { + updateRelationship(null) + } + ) + .autoDispose() + } + + + private fun updateRelationship(relationship: Relationship?) { + if (relationship != null) { + muteStateMutable.value = Success(relationship.muting) + blockStateMutable.value = Success(relationship.blocking) + } else { + muteStateMutable.value = Error(false) + blockStateMutable.value = Error(false) + } + } + + fun toggleMute() { + val alreadyMuted = muteStateMutable.value?.data == true + if (alreadyMuted) { + mastodonApi.unmuteAccount(accountId) + } else { + mastodonApi.muteAccount(accountId) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { relationship -> + val muting = relationship?.muting == true + muteStateMutable.value = Success(muting) + if (muting) { + eventHub.dispatch(MuteEvent(accountId, true)) + } + }, + { error -> + muteStateMutable.value = Error(false, error.message) + } + ).autoDispose() + + muteStateMutable.value = Loading() + } + + fun toggleBlock() { + val alreadyBlocked = blockStateMutable.value?.data == true + if (alreadyBlocked) { + mastodonApi.unblockAccount(accountId) + } else { + mastodonApi.blockAccount(accountId) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { relationship -> + val blocking = relationship?.blocking == true + blockStateMutable.value = Success(blocking) + if (blocking) { + eventHub.dispatch(BlockEvent(accountId)) + } + }, + { error -> + blockStateMutable.value = Error(false, error.message) + } + ) + .autoDispose() + + blockStateMutable.value = Loading() + } + + fun doReport() { + reportingStateMutable.value = Loading() + mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + reportingStateMutable.value = Success(true) + }, + { error -> + reportingStateMutable.value = Error(cause = error) + } + ) + .autoDispose() + + } + + fun retryStatusLoad() { + repoResult.value?.retry?.invoke() + } + + fun refreshStatuses() { + repoResult.value?.refresh?.invoke() + } + + fun checkClickedUrl(url: String?) { + checkUrlMutable.value = url + } + + fun urlChecked() { + checkUrlMutable.value = null + } + + fun setStatusChecked(status: Status, checked: Boolean) { + if (checked) { + selectedIds.add(status.id) + } else { + selectedIds.remove(status.id) + } + } + + fun isStatusChecked(id: String): Boolean { + return selectedIds.contains(id) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt new file mode 100644 index 0000000..643c46c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt @@ -0,0 +1,24 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report + +enum class Screen { + Statuses, + Note, + Done, + Back, + Finish +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt new file mode 100644 index 0000000..588cfef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import android.view.View +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.LinkListener +import java.util.ArrayList + +interface AdapterHandler: LinkListener { + fun showMedia(v: View?, status: Status?, idx: Int) + fun setStatusChecked(status: Status, isChecked: Boolean) + fun isStatusChecked(id: String): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt new file mode 100644 index 0000000..506d99a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt @@ -0,0 +1,36 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment +import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment +import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment + +class ReportPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> ReportStatusesFragment.newInstance() + 1 -> ReportNoteFragment.newInstance() + 2 -> ReportDoneFragment.newInstance() + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + override fun getItemCount() = 3 +} \ No newline at end of file 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 new file mode 100644 index 0000000..93b3a7d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -0,0 +1,178 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import android.text.Spanned +import android.text.TextUtils +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.entity.Emoji +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.LinkListener +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 statusDisplayOptions: StatusDisplayOptions, + private val viewState: StatusViewState, + private val adapterHandler: AdapterHandler, + private val getStatusForPosition: (Int) -> Status? +) : RecyclerView.ViewHolder(itemView) { + private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) + private val statusViewHelper = StatusViewHelper(itemView) + + private val previewListener = object : StatusViewHelper.MediaPreviewListener { + override fun onViewMedia(v: View?, idx: Int) { + status()?.let { status -> + adapterHandler.showMedia(v, status, idx) + } + } + + override fun onContentHiddenChange(isShowing: Boolean) { + status()?.id?.let { id -> + viewState.setMediaShow(id, isShowing) + } + } + } + + init { + itemView.statusSelection.setOnCheckedChangeListener { _, isChecked -> + status()?.let { status -> + adapterHandler.setStatusChecked(status, isChecked) + } + } + itemView.status_media_preview_container.clipToOutline = true + } + + fun bind(status: Status) { + itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + + updateTextView() + + val sensitive = status.sensitive + + statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments, + sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), + mediaViewHeight) + + statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime) + setCreatedAt(status.createdAt) + } + + private fun updateTextView() { + status()?.let { status -> + setupCollapsedState(shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), + viewState.isContentShow(status.id, status.sensitive), status.spoilerText) + + if (status.spoilerText.isBlank()) { + setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) + itemView.statusContentWarningButton.hide() + itemView.statusContentWarningDescription.hide() + } else { + val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription) + itemView.statusContentWarningDescription.text = emojiSpoiler + itemView.statusContentWarningDescription.show() + itemView.statusContentWarningButton.show() + setContentWarningButtonText(viewState.isContentShow(status.id, true)) + itemView.statusContentWarningButton.setOnClickListener { + status()?.let { status -> + val contentShown = viewState.isContentShow(status.id, true) + itemView.statusContentWarningDescription.invalidate() + viewState.setContentShow(status.id, !contentShown) + setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler) + setContentWarningButtonText(!contentShown) + } + } + setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler) + } + } + } + + private fun setContentWarningButtonText(contentShown: Boolean) { + if(contentShown) { + itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_less) + } else { + itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_more) + } + } + + private fun setTextVisible(expanded: Boolean, + content: Spanned, + mentions: Array?, + emojis: List, + listener: LinkListener) { + if (expanded) { + val emojifiedText = content.emojify(emojis, itemView.statusContent) + LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) + } else { + LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) + } + if (itemView.statusContent.text.isNullOrBlank()) { + itemView.statusContent.hide() + } else { + itemView.statusContent.show() + } + } + + private fun setCreatedAt(createdAt: Date?) { + if (statusDisplayOptions.useAbsoluteTime) { + itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) + } else { + itemView.timestampInfo.text = if (createdAt != null) { + val then = createdAt.time + val now = System.currentTimeMillis() + TimestampUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now) + } else { + // unknown minutes~ + "?m" + } + } + } + + + 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{ + status()?.let { status -> + viewState.setCollapsed(status.id, !collapsed) + updateTextView() + } + } + + itemView.buttonToggleContent.show() + if (collapsed) { + itemView.buttonToggleContent.setText(R.string.status_content_show_more) + itemView.statusContent.filters = COLLAPSE_INPUT_FILTER + } else { + itemView.buttonToggleContent.setText(R.string.status_content_show_less) + itemView.statusContent.filters = NO_INPUT_FILTER + } + } else { + itemView.buttonToggleContent.hide() + itemView.statusContent.filters = NO_INPUT_FILTER + } + } + + private fun status() = getStatusForPosition(adapterPosition) +} \ No newline at end of file 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 new file mode 100644 index 0000000..34817ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -0,0 +1,65 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +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.components.report.model.StatusViewState +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.StatusDisplayOptions + +class StatusesAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusViewState: StatusViewState, + private val adapterHandler: AdapterHandler +) : 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, + statusForPosition) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + getItem(position)?.let { status -> + (holder as? StatusViewHolder)?.bind(status) + } + + } + + companion object { + + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = + oldItem == newItem + + 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/components/report/adapter/StatusesDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt new file mode 100644 index 0000000..12fd8cc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt @@ -0,0 +1,150 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import android.annotation.SuppressLint +import androidx.lifecycle.MutableLiveData +import androidx.paging.ItemKeyedDataSource +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.NetworkState +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import java.util.concurrent.Executor + +class StatusesDataSource(private val accountId: String, + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable, + private val retryExecutor: Executor) : ItemKeyedDataSource() { + + val networkStateAfter = MutableLiveData() + val networkStateBefore = MutableLiveData() + + private var retryAfter: (() -> Any)? = null + private var retryBefore: (() -> Any)? = null + private var retryInitial: (() -> Any)? = null + + val initialLoad = MutableLiveData() + fun retryAllFailed() { + var prevRetry = retryInitial + retryInitial = null + prevRetry?.let { + retryExecutor.execute { + it.invoke() + } + } + + prevRetry = retryAfter + retryAfter = null + prevRetry?.let { + retryExecutor.execute { + it.invoke() + } + } + + prevRetry = retryBefore + retryBefore = null + prevRetry?.let { + retryExecutor.execute { + it.invoke() + } + } + } + + @SuppressLint("CheckResult") + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + networkStateAfter.postValue(NetworkState.LOADED) + networkStateBefore.postValue(NetworkState.LOADED) + retryAfter = null + retryBefore = null + retryInitial = null + initialLoad.postValue(NetworkState.LOADING) + val initialKey = params.requestedInitialKey + if (initialKey == null) { + mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true) + } else { + mastodonApi.statusObservable(initialKey).zipWith( + mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true), + BiFunction { status: Status, list: List -> + val ret = ArrayList() + ret.add(status) + ret.addAll(list) + return@BiFunction ret + }) + } + .doOnSubscribe { + disposables.add(it) + } + .subscribe( + { + callback.onResult(it) + initialLoad.postValue(NetworkState.LOADED) + }, + { + retryInitial = { + loadInitial(params, callback) + } + initialLoad.postValue(NetworkState.error(it.message)) + } + ) + } + + @SuppressLint("CheckResult") + override fun loadAfter(params: LoadParams, callback: LoadCallback) { + networkStateAfter.postValue(NetworkState.LOADING) + retryAfter = null + mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true) + .doOnSubscribe { + disposables.add(it) + } + .subscribe( + { + callback.onResult(it) + networkStateAfter.postValue(NetworkState.LOADED) + }, + { + retryAfter = { + loadAfter(params, callback) + } + networkStateAfter.postValue(NetworkState.error(it.message)) + } + ) + } + + @SuppressLint("CheckResult") + override fun loadBefore(params: LoadParams, callback: LoadCallback) { + networkStateBefore.postValue(NetworkState.LOADING) + retryBefore = null + mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true) + .doOnSubscribe { + disposables.add(it) + } + .subscribe( + { + callback.onResult(it) + networkStateBefore.postValue(NetworkState.LOADED) + }, + { + retryBefore = { + loadBefore(params, callback) + } + networkStateBefore.postValue(NetworkState.error(it.message)) + } + ) + } + + override fun getKey(item: Status): String = item.id +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt new file mode 100644 index 0000000..4cf8ff1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt @@ -0,0 +1,36 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.Executor + +class StatusesDataSourceFactory( + private val accountId: String, + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable, + private val retryExecutor: Executor) : DataSource.Factory() { + val sourceLiveData = MutableLiveData() + override fun create(): DataSource { + val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor) + sourceLiveData.postValue(source) + return source + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt new file mode 100644 index 0000000..852a07f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt @@ -0,0 +1,61 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.adapter + +import androidx.lifecycle.Transformations +import androidx.paging.Config +import androidx.paging.toLiveData +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.BiListing +import com.keylesspalace.tusky.util.Listing +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.Executors +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StatusesRepository @Inject constructor(private val mastodonApi: MastodonApi) { + + private val executor = Executors.newSingleThreadExecutor() + + fun getStatuses(accountId: String, initialStatus: String?, disposables: CompositeDisposable, pageSize: Int = 20): BiListing { + val sourceFactory = StatusesDataSourceFactory(accountId, mastodonApi, disposables, executor) + val livePagedList = sourceFactory.toLiveData( + config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), + fetchExecutor = executor, initialLoadKey = initialStatus + ) + return BiListing( + pagedList = livePagedList, + networkStateBefore = Transformations.switchMap(sourceFactory.sourceLiveData) { + it.networkStateBefore + }, + networkStateAfter = Transformations.switchMap(sourceFactory.sourceLiveData) { + it.networkStateAfter + }, + retry = { + sourceFactory.sourceLiveData.value?.retryAllFailed() + }, + refresh = { + sourceFactory.sourceLiveData.value?.invalidate() + }, + refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { + it.initialLoad + } + + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..410f9dd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -0,0 +1,96 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +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.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 javax.inject.Inject + +class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) + handleClicks() + subscribeObservables() + } + + private fun subscribeObservables() { + viewModel.muteState.observe(viewLifecycleOwner, Observer { + if (it !is Loading) { + buttonMute.show() + progressMute.show() + } else { + buttonMute.hide() + progressMute.hide() + } + + buttonMute.setText(when (it.data) { + true -> R.string.action_unmute + else -> R.string.action_mute + }) + }) + + viewModel.blockState.observe(viewLifecycleOwner, Observer { + if (it !is Loading) { + buttonBlock.show() + progressBlock.show() + } + else{ + buttonBlock.hide() + progressBlock.hide() + } + buttonBlock.setText(when (it.data) { + true -> R.string.action_unblock + else -> R.string.action_block + }) + }) + + } + + private fun handleClicks() { + buttonDone.setOnClickListener { + viewModel.navigateTo(Screen.Finish) + } + buttonBlock.setOnClickListener { + viewModel.toggleBlock() + } + buttonMute.setOnClickListener { + viewModel.toggleMute() + } + } + + 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 new file mode 100644 index 0000000..e13fc81 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -0,0 +1,128 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.fragments + +import android.os.Bundle +import android.view.View +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.fragment.app.activityViewModels +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.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 + +class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + fillViews() + handleChanges() + handleClicks() + subscribeObservables() + } + + private fun handleChanges() { + editNote.doAfterTextChanged { + viewModel.reportNote = it?.toString() ?: "" + } + checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> + viewModel.isRemoteNotify = isChecked + } + } + + private fun fillViews() { + editNote.setText(viewModel.reportNote) + + if (viewModel.isRemoteAccount){ + checkIsNotifyRemote.show() + reportDescriptionRemoteInstance.show() + } + else{ + checkIsNotifyRemote.hide() + reportDescriptionRemoteInstance.hide() + } + + if (viewModel.isRemoteAccount) + checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) + checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify + } + + private fun subscribeObservables() { + viewModel.reportingState.observe(viewLifecycleOwner, Observer { + when (it) { + is Success -> viewModel.navigateTo(Screen.Done) + is Loading -> showLoading() + is Error -> showError(it.cause) + + } + }) + } + + private fun showError(error: Throwable?) { + editNote.isEnabled = true + checkIsNotifyRemote.isEnabled = true + buttonReport.isEnabled = true + buttonBack.isEnabled = true + progressBar.hide() + + Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) + .apply { + setAction(R.string.action_retry) { + sendReport() + } + } + .show() + } + + private fun sendReport() { + viewModel.doReport() + } + + private fun showLoading() { + buttonReport.isEnabled = false + buttonBack.isEnabled = false + editNote.isEnabled = false + checkIsNotifyRemote.isEnabled = false + progressBar.show() + } + + private fun handleClicks() { + buttonBack.setOnClickListener { + viewModel.navigateTo(Screen.Back) + } + + buttonReport.setOnClickListener { + sendReport() + } + } + + 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 new file mode 100644 index 0000000..ad4b6ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -0,0 +1,200 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.fragments + +import android.os.Bundle +import android.view.View +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.paging.PagedList +import androidx.fragment.app.activityViewModels +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.AccountActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.ViewTagActivity +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.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +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 { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + @Inject + lateinit var accountManager: AccountManager + + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + + private lateinit var adapter: StatusesAdapter + + private var snackbarErrorRetry: Snackbar? = null + + override fun showMedia(v: View?, status: Status?, idx: Int) { + status?.actionableStatus?.let { actionable -> + when (actionable.attachments[idx].type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { + val attachments = AttachmentViewData.list(actionable) + val intent = ViewMediaActivity.newIntent(context, attachments, + idx) + if (v != null) { + val url = actionable.attachments[idx].url + ViewCompat.setTransitionName(v, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), + v, url) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Attachment.Type.UNKNOWN -> { + } + } + + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + handleClicks() + initStatusesView() + setupSwipeRefreshLayout() + } + + private fun setupSwipeRefreshLayout() { + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + swipeRefreshLayout.setOnRefreshListener { + snackbarErrorRetry?.dismiss() + viewModel.refreshStatuses() + } + } + + private fun initStatusesView() { + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = false, + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = false, + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + renderStatusAsMention = preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + ) + + 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 + + viewModel.statuses.observe(viewLifecycleOwner, Observer> { + adapter.submitList(it) + }) + + viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer { + if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) + progressBarBottom.show() + else + progressBarBottom.hide() + + if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) + showError(it.msg) + }) + + viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer { + if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) + progressBarTop.show() + else + progressBarTop.hide() + + if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) + showError(it.msg) + }) + + viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer { + if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) + progressBarLoading.show() + else + progressBarLoading.hide() + + if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) + swipeRefreshLayout.isRefreshing = false + if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) + showError(it.msg) + }) + } + + 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?.setAction(R.string.action_retry) { + viewModel.retryStatusLoad() + } + snackbarErrorRetry?.show() + } + } + + + private fun handleClicks() { + buttonCancel.setOnClickListener { + viewModel.navigateTo(Screen.Back) + } + + buttonContinue.setOnClickListener { + viewModel.navigateTo(Screen.Note) + } + } + + override fun setStatusChecked(status: Status, isChecked: Boolean) { + viewModel.setStatusChecked(status, isChecked) + } + + override fun isStatusChecked(id: String): Boolean { + return viewModel.isStatusChecked(id) + } + + override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) + + override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag)) + + override fun onViewUrl(url: String?) = viewModel.checkClickedUrl(url) + + companion object { + fun newInstance() = ReportStatusesFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt new file mode 100644 index 0000000..664ddc6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt @@ -0,0 +1,36 @@ +/* Copyright 2019 Joel Pyska + * + * 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.report.model + +class StatusViewState { + private val mediaShownState = HashMap() + private val contentShownState = HashMap() + private val longContentCollapsedState = HashMap() + + fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(mediaShownState, id, !isSensitive) + fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow) + + fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(contentShownState, id, !isSensitive) + fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow) + + fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed) + fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed) + + private fun isStateEnabled(map: Map, id: String, def: Boolean): Boolean = map[id] + ?: def + + private fun setStateEnabled(map: MutableMap, id: String, state: Boolean) = map.put(id, state) +} \ No newline at end of file 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 new file mode 100644 index 0000000..29f16fa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -0,0 +1,149 @@ +/* Copyright 2019 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.scheduled + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +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.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.ThemeUtils +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 { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + lateinit var viewModel: ScheduledTootViewModel + + private val adapter = ScheduledTootAdapter(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_scheduled_toot) + + setSupportActionBar(toolbar) + supportActionBar?.run { + title = getString(R.string.title_scheduled_toot) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + scheduledTootList.setHasFixedSize(true) + scheduledTootList.layoutManager = LinearLayoutManager(this) + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + scheduledTootList.addItemDecoration(divider) + scheduledTootList.adapter = adapter + + viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java] + + viewModel.data.observe(this, Observer { + adapter.submitList(it) + }) + + viewModel.networkState.observe(this, Observer { (status) -> + when(status) { + Status.SUCCESS -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + if(viewModel.data.value?.loadedCount == 0) { + errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + errorMessageView.show() + } else { + errorMessageView.hide() + } + } + Status.RUNNING -> { + errorMessageView.hide() + if(viewModel.data.value?.loadedCount ?: 0 > 0) { + swipeRefreshLayout.isRefreshing = true + } else { + 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) { + refreshStatuses() + } + errorMessageView.show() + } + } + } + + }) + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun refreshStatuses() { + viewModel.reload() + } + + override fun edit(item: ScheduledStatus) { + val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( + scheduledTootId = item.id, + tootText = item.params.text, + contentWarning = item.params.spoilerText, + mediaAttachments = item.mediaAttachments, + inReplyToId = item.params.inReplyToId, + visibility = item.params.visibility, + scheduledAt = item.scheduledAt, + sensitive = item.params.sensitive + )) + startActivity(intent) + } + + override fun delete(item: ScheduledStatus) { + viewModel.deleteScheduledStatus(item) + } + + companion object { + @JvmStatic + fun newIntent(context: Context): Intent { + return Intent(context, ScheduledTootActivity::class.java) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt new file mode 100644 index 0000000..ea12d1f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -0,0 +1,85 @@ +/* Copyright 2019 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.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.entity.ScheduledStatus + +interface ScheduledTootActionListener { + fun edit(item: ScheduledStatus) + fun delete(item: ScheduledStatus) +} + +class ScheduledTootAdapter( + val listener: ScheduledTootActionListener +) : PagedListAdapter( + object: DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem == newItem + } + + } +) { + + 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 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 -> + v.isEnabled = false + listener.edit(item) + } + 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/scheduled/ScheduledTootDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt new file mode 100644 index 0000000..6c9ba31 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt @@ -0,0 +1,102 @@ +/* Copyright 2019 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.scheduled + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import androidx.paging.ItemKeyedDataSource +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.NetworkState +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo + +class ScheduledTootDataSourceFactory( + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable +): DataSource.Factory() { + + private val scheduledTootsCache = mutableListOf() + + private var dataSource: ScheduledTootDataSource? = null + + val networkState = MutableLiveData() + + override fun create(): DataSource { + return ScheduledTootDataSource(mastodonApi, disposables, scheduledTootsCache, networkState).also { + dataSource = it + } + } + + fun reload() { + scheduledTootsCache.clear() + dataSource?.invalidate() + } + + fun remove(status: ScheduledStatus) { + scheduledTootsCache.remove(status) + dataSource?.invalidate() + } + +} + + +class ScheduledTootDataSource( + private val mastodonApi: MastodonApi, + private val disposables: CompositeDisposable, + private val scheduledTootsCache: MutableList, + private val networkState: MutableLiveData +): ItemKeyedDataSource() { + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + if(scheduledTootsCache.isNotEmpty()) { + callback.onResult(scheduledTootsCache.toList()) + } else { + networkState.postValue(NetworkState.LOADING) + mastodonApi.scheduledStatuses(limit = params.requestedLoadSize) + .subscribe({ newData -> + scheduledTootsCache.addAll(newData) + callback.onResult(newData) + networkState.postValue(NetworkState.LOADED) + }, { throwable -> + Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) + networkState.postValue(NetworkState.error(throwable.message)) + }) + .addTo(disposables) + } + } + + override fun loadAfter(params: LoadParams, callback: LoadCallback) { + mastodonApi.scheduledStatuses(limit = params.requestedLoadSize, maxId = params.key) + .subscribe({ newData -> + scheduledTootsCache.addAll(newData) + callback.onResult(newData) + }, { throwable -> + Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) + networkState.postValue(NetworkState.error(throwable.message)) + }) + .addTo(disposables) + } + + override fun loadBefore(params: LoadParams, callback: LoadCallback) { + // we are always loading from beginning to end + } + + override fun getKey(item: ScheduledStatus): String { + return item.id + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt new file mode 100644 index 0000000..3584168 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -0,0 +1,68 @@ +/* Copyright 2019 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.scheduled + +import android.util.Log +import androidx.paging.Config +import androidx.paging.toLiveData +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusScheduledEvent +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.RxAwareViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import javax.inject.Inject + +class ScheduledTootViewModel @Inject constructor( + val mastodonApi: MastodonApi, + val eventHub: EventHub +): RxAwareViewModel() { + + private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables) + + val data = dataSourceFactory.toLiveData( + config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false) + ) + + val networkState = dataSourceFactory.networkState + + init { + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { event -> + if (event is StatusScheduledEvent) { + reload() + } + } + .autoDispose() + } + + fun reload() { + dataSourceFactory.reload() + } + + fun deleteScheduledStatus(status: ScheduledStatus) { + mastodonApi.deleteScheduledStatus(status.id) + .subscribe({ + dataSourceFactory.remove(status) + },{ throwable -> + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + }) + .autoDispose() + + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..8b8d1ef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -0,0 +1,126 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.search + +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import com.google.android.material.tabs.TabLayoutMediator +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter +import com.keylesspalace.tusky.di.ViewModelFactory +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import kotlinx.android.synthetic.main.activity_search.* +import javax.inject.Inject + +class SearchActivity : BottomSheetActivity(), HasAndroidInjector { + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: SearchViewModel by viewModels { viewModelFactory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + setSupportActionBar(toolbar) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setDisplayShowTitleEnabled(false) + } + setupPages() + handleIntent(intent) + } + + private fun setupPages() { + pages.adapter = SearchPagerAdapter(this) + + TabLayoutMediator(tabs, pages) { + tab, position -> + tab.text = getPageTitle(position) + }.attach() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntent(intent) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + menuInflater.inflate(R.menu.search_toolbar, menu) + val searchView = menu.findItem(R.id.action_search) + .actionView as SearchView + setupSearchView(searchView) + + searchView.setQuery(viewModel.currentQuery, false) + + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun getPageTitle(position: Int): CharSequence? { + return when (position) { + 0 -> getString(R.string.title_statuses) + 1 -> getString(R.string.title_accounts) + 2 -> getString(R.string.title_hashtags_dialog) + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + private fun handleIntent(intent: Intent) { + if (Intent.ACTION_SEARCH == intent.action) { + viewModel.currentQuery = intent.getStringExtra(SearchManager.QUERY) ?: "" + viewModel.search(viewModel.currentQuery) + } + } + + private fun setupSearchView(searchView: SearchView) { + searchView.setIconifiedByDefault(false) + + searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName)) + + searchView.requestFocus() + + searchView.maxWidth = Integer.MAX_VALUE + } + + override fun androidInjector() = androidInjector + + companion object { + fun getIntent(context: Context) = Intent(context, SearchActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt new file mode 100644 index 0000000..5df6574 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.components.search + +enum class SearchType(val apiParameter: String) { + Status("statuses"), + Account("accounts"), + Hashtag("hashtags") +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt new file mode 100644 index 0000000..8394457 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -0,0 +1,242 @@ +package com.keylesspalace.tusky.components.search + +import android.util.Log +import android.view.View +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.PagedList +import com.keylesspalace.tusky.components.search.adapter.SearchRepository +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewdata.StatusViewData +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import javax.inject.Inject + +class SearchViewModel @Inject constructor( + mastodonApi: MastodonApi, + private val timelineCases: TimelineCases, + private val accountManager: AccountManager +) : RxAwareViewModel() { + + var currentQuery: String = "" + + var activeAccount: AccountEntity? + get() = accountManager.activeAccount + set(value) { + accountManager.activeAccount = value + } + + val mediaPreviewEnabled = activeAccount?.mediaPreviewEnabled ?: false + val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false + val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false + + private val statusesRepository = SearchRepository>(mastodonApi) + private val accountsRepository = SearchRepository(mastodonApi) + private val hashtagsRepository = SearchRepository(mastodonApi) + + private val repoResultStatus = MutableLiveData>>() + val statuses: LiveData>> = repoResultStatus.switchMap { it.pagedList } + val networkStateStatus: LiveData = repoResultStatus.switchMap { it.networkState } + val networkStateStatusRefresh: LiveData = repoResultStatus.switchMap { it.refreshState } + + private val repoResultAccount = MutableLiveData>() + val accounts: LiveData> = repoResultAccount.switchMap { it.pagedList } + val networkStateAccount: LiveData = repoResultAccount.switchMap { it.networkState } + val networkStateAccountRefresh: LiveData = repoResultAccount.switchMap { it.refreshState } + + private val repoResultHashTag = MutableLiveData>() + val hashtags: LiveData> = repoResultHashTag.switchMap { it.pagedList } + val networkStateHashTag: LiveData = repoResultHashTag.switchMap { it.networkState } + val networkStateHashTagRefresh: LiveData = repoResultHashTag.switchMap { it.refreshState } + + private val loadedStatuses = ArrayList>() + fun search(query: String) { + loadedStatuses.clear() + repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) { + it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) } + .orEmpty() + .apply { + loadedStatuses.addAll(this) + } + } + repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) { + it?.accounts.orEmpty() + } + val hashtagQuery = if (query.startsWith("#")) query else "#$query" + repoResultHashTag.value = + hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) { + it?.hashtags.orEmpty() + } + + } + + fun removeItem(status: Pair) { + timelineCases.delete(status.first.id) + .subscribe({ + if (loadedStatuses.remove(status)) + repoResultStatus.value?.refresh?.invoke() + }, { + err -> Log.d(TAG, "Failed to delete status", err) + }) + .autoDispose() + + } + + fun expandedChange(status: Pair, expanded: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setIsExpanded(expanded).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + } + + fun reblog(status: Pair, reblog: Boolean) { + timelineCases.reblog(status.first, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { setRebloggedForStatus(status, reblog) }, + { err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) } + ) + .autoDispose() + } + + private fun setRebloggedForStatus(status: Pair, reblog: Boolean) { + status.first.reblogged = reblog + status.first.reblog?.reblogged = reblog + + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setReblogged(reblog).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + } + + fun contentHiddenChange(status: Pair, isShowing: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setIsShowingSensitiveContent(isShowing).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + } + + fun collapsedChange(status: Pair, collapsed: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setCollapsed(collapsed).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + } + + fun voteInPoll(status: Pair, choices: MutableList) { + val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices) + updateStatus(status, votedPoll) + timelineCases.voteInPoll(status.first, choices) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { newPoll -> updateStatus(status, newPoll) }, + { t -> + Log.d(TAG, + "Failed to vote in poll: ${status.first.id}", t) + } + ) + .autoDispose() + } + + private fun updateStatus(status: Pair, newPoll: Poll) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + + val newViewData = StatusViewData.Builder(status.second) + .setPoll(newPoll) + .createStatusViewData() + loadedStatuses[idx] = Pair(status.first, newViewData) + repoResultStatus.value?.refresh?.invoke() + } + } + + fun favorite(status: Pair, isFavorited: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isFavorited).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + timelineCases.favourite(status.first, isFavorited) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() + } + + fun bookmark(status: Pair, isBookmarked: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setBookmarked(isBookmarked).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + timelineCases.bookmark(status.first, isBookmarked) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() + } + + fun getAllAccountsOrderedByActive(): List { + return accountManager.getAllAccountsOrderedByActive() + } + + fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { + timelineCases.mute(accountId, notifications, duration) + } + + fun muteConversation(status: Status, isMute: Boolean) { + timelineCases.muteConversation(status, isMute) + } + + fun pinAccount(status: Status, isPin: Boolean) { + timelineCases.pin(status, isPin) + } + + fun blockAccount(accountId: String) { + timelineCases.block(accountId) + } + + fun deleteStatus(id: String): Single { + return timelineCases.delete(id) + } + + fun retryAllSearches() { + search(currentQuery) + } + + fun setEmojiReactionForStatus(idx: Int, newStatus: Status) { + val newPair = Pair(newStatus, + ViewDataUtils.statusToViewData(newStatus, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + + fun emojiReact(react: Boolean, emoji: String, statusId: String) { + loadedStatuses.indexOfFirst { it.first.id == statusId }.let { idx -> + timelineCases.react(emoji, statusId, react) + .subscribe( + { newStatus -> setEmojiReactionForStatus(idx, newStatus)}, + { Log.d(TAG,"Failed to react with $emoji to ${loadedStatuses[idx].first.id}", it)} + ) + .autoDispose() + } + } + + companion object { + private const val TAG = "SearchViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt new file mode 100644 index 0000000..c135ad7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -0,0 +1,58 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +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.AccountViewHolder +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.interfaces.LinkListener + +class SearchAccountsAdapter(private val linkListener: LinkListener) + : PagedListAdapter(ACCOUNT_COMPARATOR) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_account, parent, false) + return AccountViewHolder(view) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + getItem(position)?.let { item -> + (holder as AccountViewHolder).apply { + setupWithAccount(item) + setupLinkListener(linkListener) + } + } + } + + companion object { + + val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean = + oldItem.deepEquals(newItem) + + override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean = + oldItem.id == newItem.id + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt new file mode 100644 index 0000000..2b70628 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt @@ -0,0 +1,126 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +import androidx.lifecycle.MutableLiveData +import androidx.paging.PositionalDataSource +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.NetworkState +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import java.util.concurrent.Executor + +class SearchDataSource( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val searchRequest: String, + private val disposables: CompositeDisposable, + private val retryExecutor: Executor, + private val initialItems: List? = null, + private val parser: (SearchResult?) -> List, + private val source: SearchDataSourceFactory) : PositionalDataSource() { + + val networkState = MutableLiveData() + + private var retry: (() -> Any)? = null + + val initialLoad = MutableLiveData() + + fun retry() { + retry?.let { + retryExecutor.execute { + it.invoke() + } + } + } + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + if (!initialItems.isNullOrEmpty()) { + callback.onResult(initialItems.toList(), 0) + } else { + networkState.postValue(NetworkState.LOADED) + retry = null + initialLoad.postValue(NetworkState.LOADING) + mastodonApi.searchObservable( + query = searchRequest, + type = searchType.apiParameter, + resolve = true, + limit = params.requestedLoadSize, + offset = 0, + following = false) + .subscribe( + { data -> + val res = parser(data) + callback.onResult(res, params.requestedStartPosition) + initialLoad.postValue(NetworkState.LOADED) + + }, + { error -> + retry = { + loadInitial(params, callback) + } + initialLoad.postValue(NetworkState.error(error.message)) + } + ).addTo(disposables) + } + + } + + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { + networkState.postValue(NetworkState.LOADING) + retry = null + if (source.exhausted) { + return callback.onResult(emptyList()) + } + mastodonApi.searchObservable( + query = searchRequest, + type = searchType.apiParameter, + resolve = true, + limit = params.loadSize, + offset = params.startPosition, + following = false) + .subscribe( + { data -> + // Working around Mastodon bug where exact match is returned no matter + // which offset is requested (so if we search for a full username, it's + // infinite) + // see https://github.com/tootsuite/mastodon/issues/11365 + // see https://github.com/tootsuite/mastodon/issues/13083 + val res = if ((data.accounts.size == 1 && data.accounts[0].username.equals(searchRequest, ignoreCase = true)) + || (data.statuses.size == 1 && data.statuses[0].url.equals(searchRequest))) { + listOf() + } else { + parser(data) + } + if (res.isEmpty()) { + source.exhausted = true + } + callback.onResult(res) + networkState.postValue(NetworkState.LOADED) + }, + { error -> + retry = { + loadRange(params, callback) + } + networkState.postValue(NetworkState.error(error.message)) + } + ).addTo(disposables) + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt new file mode 100644 index 0000000..b47da70 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt @@ -0,0 +1,44 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.Executor + +class SearchDataSourceFactory( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val searchRequest: String, + private val disposables: CompositeDisposable, + private val retryExecutor: Executor, + private val cacheData: List? = null, + private val parser: (SearchResult?) -> List) : DataSource.Factory() { + + val sourceLiveData = MutableLiveData>() + + var exhausted = false + + override fun create(): DataSource { + val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser, this) + sourceLiveData.postValue(source) + return source + } +} \ 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 new file mode 100644 index 0000000..71863d4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -0,0 +1,55 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +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.entity.HashTag +import com.keylesspalace.tusky.interfaces.LinkListener + +class SearchHashtagsAdapter(private val linkListener: LinkListener) + : 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 onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + getItem(position)?.let { (name) -> + (holder as HashtagViewHolder).setup(name, linkListener) + } + } + + companion object { + + val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = + oldItem.name == newItem.name + + override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = + oldItem.name == newItem.name + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt new file mode 100644 index 0000000..845abaf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt @@ -0,0 +1,38 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment + +class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> SearchStatusesFragment.newInstance() + 1 -> SearchAccountsFragment.newInstance() + 2 -> SearchHashtagsFragment.newInstance() + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + override fun getItemCount() = 3 + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt new file mode 100644 index 0000000..28d9564 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt @@ -0,0 +1,56 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +import androidx.lifecycle.Transformations +import androidx.paging.Config +import androidx.paging.toLiveData +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Listing +import io.reactivex.disposables.CompositeDisposable +import java.util.concurrent.Executors + +class SearchRepository(private val mastodonApi: MastodonApi) { + + private val executor = Executors.newSingleThreadExecutor() + + fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20, + initialItems: List? = null, parser: (SearchResult?) -> List): Listing { + val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser) + val livePagedList = sourceFactory.toLiveData( + config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), + fetchExecutor = executor + ) + return Listing( + pagedList = livePagedList, + networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { + it.networkState + }, + retry = { + sourceFactory.sourceLiveData.value?.retry() + }, + refresh = { + sourceFactory.sourceLiveData.value?.invalidate() + }, + refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { + it.initialLoad + } + + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt new file mode 100644 index 0000000..0fcee37 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -0,0 +1,63 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.adapter + +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.StatusViewHolder +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class SearchStatusesAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusListener: StatusActionListener +) : PagedListAdapter, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status, parent, false) + return StatusViewHolder(view) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + getItem(position)?.let { item -> + (holder as StatusViewHolder).setupWithStatus(item.second, statusListener, statusDisplayOptions) + } + } + + public override fun getItem(position: Int): Pair? { + return super.getItem(position) + } + + companion object { + + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback>() { + override fun areContentsTheSame(oldItem: Pair, newItem: Pair): Boolean = + oldItem.second.deepEquals(newItem.second) + + override fun areItemsTheSame(oldItem: Pair, newItem: Pair): Boolean = + oldItem.second.id == newItem.second.id + } + + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..714580f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -0,0 +1,39 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.fragments + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import androidx.paging.PagedListAdapter +import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.util.NetworkState + +class SearchAccountsFragment : SearchFragment() { + override fun createAdapter(): PagedListAdapter = SearchAccountsAdapter(this) + + override val networkStateRefresh: LiveData + get() = viewModel.networkStateAccountRefresh + override val networkState: LiveData + get() = viewModel.networkStateAccount + override val data: LiveData> + get() = viewModel.accounts + + 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 new file mode 100644 index 0000000..faae08e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -0,0 +1,132 @@ +package com.keylesspalace.tusky.components.search.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.paging.PagedList +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.AccountActivity +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.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), + LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } + + private var snackbarErrorRetry: Snackbar? = null + + abstract fun createAdapter(): PagedListAdapter + + abstract val networkStateRefresh: LiveData + abstract val networkState: LiveData + abstract val data: LiveData> + protected lateinit var adapter: PagedListAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initAdapter() + setupSwipeRefreshLayout() + subscribeObservables() + } + + private fun setupSwipeRefreshLayout() { + swipeRefreshLayout.setOnRefreshListener(this) + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + + private fun subscribeObservables() { + data.observe(viewLifecycleOwner, Observer { + adapter.submitList(it) + }) + + networkStateRefresh.observe(viewLifecycleOwner, Observer { + + searchProgressBar.visible(it == NetworkState.LOADING) + + if (it.status == Status.FAILED) { + showError() + } + checkNoData() + + }) + + networkState.observe(viewLifecycleOwner, Observer { + + progressBarBottom.visible(it == NetworkState.LOADING) + + if (it.status == Status.FAILED) { + showError() + } + }) + } + + private fun checkNoData() { + showNoData(adapter.itemCount == 0) + } + + private fun initAdapter() { + searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) + searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) + adapter = createAdapter() + searchRecyclerView.adapter = adapter + searchRecyclerView.setHasFixedSize(true) + (searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + private fun showNoData(isEmpty: Boolean) { + if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) + searchNoResultsText.show() + else + searchNoResultsText.hide() + } + + private fun showError() { + if (snackbarErrorRetry?.isShown != true) { + snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry?.setAction(R.string.action_retry) { + snackbarErrorRetry = null + viewModel.retryAllSearches() + } + snackbarErrorRetry?.show() + } + } + + override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) + + override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag)) + + override fun onViewUrl(url: String) { + bottomSheetActivity?.viewUrl(url) + } + + protected val bottomSheetActivity + get() = (activity as? BottomSheetActivity) + + override fun onRefresh() { + + // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. + swipeRefreshLayout.post { + swipeRefreshLayout.isRefreshing = false + } + viewModel.retryAllSearches() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt new file mode 100644 index 0000000..15310d3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt @@ -0,0 +1,38 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.fragments + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import androidx.paging.PagedListAdapter +import com.keylesspalace.tusky.components.search.adapter.SearchHashtagsAdapter +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.util.NetworkState + +class SearchHashtagsFragment : SearchFragment() { + override val networkStateRefresh: LiveData + get() = viewModel.networkStateHashTagRefresh + override val networkState: LiveData + get() = viewModel.networkStateHashTag + override val data: LiveData> + get() = viewModel.hashtags + + override fun createAdapter(): PagedListAdapter = SearchHashtagsAdapter(this) + + companion object { + fun newInstance() = SearchHashtagsFragment() + } +} 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 new file mode 100644 index 0000000..8f6a449 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -0,0 +1,517 @@ +/* Copyright 2019 Joel Pyska + * + * 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.search.fragments + +import android.Manifest +import android.app.DownloadManager +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Environment +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.CheckBox +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import androidx.paging.PagedListAdapter +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.EmojiReaction +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Status.Mention +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.NetworkState +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.view.showMuteAccountDialog +import com.keylesspalace.tusky.viewdata.AttachmentViewData +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 { + + override val networkStateRefresh: LiveData + get() = viewModel.networkStateStatusRefresh + override val networkState: LiveData + get() = viewModel.networkStateStatus + override val data: LiveData>> + get() = viewModel.statuses + + private val searchAdapter + get() = super.adapter as SearchStatusesAdapter + + override fun createAdapter(): PagedListAdapter, *> { + val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = viewModel.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = preferences.getBoolean("showBotOverlay", true), + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + renderStatusAsMention = preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + ) + + searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) + searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) + return SearchStatusesAdapter(statusDisplayOptions, this) + } + + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { + viewModel.contentHiddenChange(it, isShowing) + } + } + + override fun onReply(position: Int) { + searchAdapter.getItem(position)?.first?.let { status -> + reply(status) + } + } + + override fun onFavourite(favourite: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { status -> + viewModel.favorite(status, favourite) + } + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { status -> + viewModel.bookmark(status, bookmark) + } + } + + override fun onMore(view: View, position: Int) { + searchAdapter.getItem(position)?.first?.let { + more(it, view, position) + } + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + searchAdapter.getItem(position)?.first?.actionableStatus?.let { actionable -> + when (actionable.attachments[attachmentIndex].type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { + val attachments = AttachmentViewData.list(actionable) + val intent = ViewMediaActivity.newIntent(context, attachments, + attachmentIndex) + if (view != null) { + val url = actionable.attachments[attachmentIndex].url + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), + view, url) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Attachment.Type.UNKNOWN -> { + LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context) + } + } + + } + + } + + override fun onViewThread(position: Int) { + searchAdapter.getItem(position)?.first?.let { status -> + val actionableStatus = status.actionableStatus + bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url) + } + } + + override fun onViewReplyTo(position: Int) { + searchAdapter.getItem(position)?.first?.let { status -> + val actionableStatus = status.actionableStatus + bottomSheetActivity?.viewThread(actionableStatus.inReplyToId!!, null) + } + } + + override fun onOpenReblog(position: Int) { + searchAdapter.getItem(position)?.first?.let { status -> + bottomSheetActivity?.viewAccount(status.account.id) + } + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { + viewModel.expandedChange(it, expanded) + } + } + + override fun onLoadMore(position: Int) { + // Not possible here + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { + viewModel.collapsedChange(it, isCollapsed) + } + } + + override fun onVoteInPoll(position: Int, choices: MutableList) { + searchAdapter.getItem(position)?.let { + viewModel.voteInPoll(it, choices) + } + } + + private fun removeItem(position: Int) { + searchAdapter.getItem(position)?.let { + viewModel.removeItem(it) + } + } + + override fun onReblog(reblog: Boolean, position: Int) { + searchAdapter.getItem(position)?.let { status -> + viewModel.reblog(status, reblog) + } + } + + companion object { + fun newInstance() = SearchStatusesFragment() + } + + private fun reply(status: Status) { + val actionableStatus = status.actionableStatus + val mentionedUsernames = actionableStatus.mentions.map { it.username } + .toMutableSet() + .apply { + add(actionableStatus.account.username) + remove(viewModel.activeAccount?.username) + } + + val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions( + inReplyToId = status.actionableId, + replyVisibility = actionableStatus.visibility, + contentWarning = actionableStatus.spoilerText, + mentionedUsernames = mentionedUsernames, + replyingStatusAuthor = actionableStatus.account.localUsername, + replyingStatusContent = actionableStatus.content.toString() + )) + startActivity(intent) + } + + private fun more(status: Status, view: View, position: Int) { + val id = status.actionableId + val accountId = status.actionableStatus.account.id + val accountUsername = status.actionableStatus.account.username + val statusUrl = status.actionableStatus.url + val accounts = viewModel.getAllAccountsOrderedByActive() + var openAsTitle: String? = null + + val loggedInAccountId = viewModel.activeAccount?.accountId + + val popup = PopupMenu(view.context, view) + // Give a different menu depending on whether this is the user's own toot or not. + if (loggedInAccountId == null || loggedInAccountId != accountId) { + popup.inflate(R.menu.status_more) + val menu = popup.menu + menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty() + } else { + popup.inflate(R.menu.status_more_for_user) + val menu = popup.menu + menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank() + when (status.visibility) { + Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> { + val textId = getString(if (status.isPinned()) R.string.unpin_action else R.string.pin_action) + menu.add(0, R.id.pin, 1, textId) + } + Status.Visibility.PRIVATE -> { + var reblogged = status.reblogged + if (status.reblog != null) reblogged = status.reblog.reblogged + menu.findItem(R.id.status_reblog_private).isVisible = !reblogged + menu.findItem(R.id.status_unreblog_private).isVisible = reblogged + } + Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> { + } //Ignore + } + } + + val openAsItem = popup.menu.findItem(R.id.status_open_as) + when (accounts.size) { + 0, 1 -> openAsItem.isVisible = false + 2 -> for (account in accounts) { + if (account !== viewModel.activeAccount) { + openAsTitle = String.format(getString(R.string.action_open_as), account.fullName) + break + } + } + else -> openAsTitle = String.format(getString(R.string.action_open_as), "…") + } + openAsItem.title = openAsTitle + + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.status_share_content -> { + val statusToShare: Status = status.actionableStatus + + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + + val stringToShare = statusToShare.account.username + + " - " + + statusToShare.content.toString() + sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare) + sendIntent.type = "text/plain" + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_content_to))) + return@setOnMenuItemClickListener true + } + R.id.status_share_link -> { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl) + sendIntent.type = "text/plain" + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_link_to))) + return@setOnMenuItemClickListener true + } + R.id.status_copy_link -> { + val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl)) + return@setOnMenuItemClickListener true + } + R.id.status_open_in_web -> { + LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), context); + return@setOnMenuItemClickListener true + } + R.id.status_open_as -> { + showOpenAsDialog(statusUrl!!, item.title) + return@setOnMenuItemClickListener true + } + R.id.status_download_media -> { + requestDownloadAllMedia(status) + return@setOnMenuItemClickListener true + } + R.id.status_mute_conversation -> { + searchAdapter.getItem(position)?.let { foundStatus -> + viewModel.muteConversation(foundStatus.first, status.muted != true) + } + return@setOnMenuItemClickListener true + } + R.id.status_mute -> { + onMute(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + R.id.status_block -> { + onBlock(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + R.id.status_report -> { + openReportPage(accountId, accountUsername, id) + return@setOnMenuItemClickListener true + } + R.id.status_unreblog_private -> { + onReblog(false, position) + return@setOnMenuItemClickListener true + } + R.id.status_reblog_private -> { + onReblog(true, position) + return@setOnMenuItemClickListener true + } + R.id.status_delete -> { + showConfirmDeleteDialog(id, position) + return@setOnMenuItemClickListener true + } + R.id.pin -> { + viewModel.pinAccount(status, !status.isPinned()) + return@setOnMenuItemClickListener true + } + } + false + } + popup.show() + } + + private fun onBlock(accountId: String, accountUsername: String) { + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun onMute(accountId: String, accountUsername: String) { + showMuteAccountDialog( + this.requireActivity(), + accountUsername + ) { notifications, duration -> + viewModel.muteAccount(accountId, notifications, duration) + } + } + + private fun accountIsInMentions(account: AccountEntity?, mentions: Array): Boolean { + return mentions.firstOrNull { + account?.username == it.username && account.domain == Uri.parse(it.url)?.host + } != null + } + + private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) { + bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + openAsAccount(statusUrl, account) + } + }) + } + + private fun openAsAccount(statusUrl: String, account: AccountEntity) { + viewModel.activeAccount = account + val intent = Intent(context, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + intent.putExtra(MainActivity.STATUS_URL, statusUrl) + startActivity(intent) + (activity as BaseActivity).finishWithoutSlideOutAnimation() + } + + private fun downloadAllMedia(status: Status) { + Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() + for ((_, url) in status.attachments) { + val uri = Uri.parse(url) + val filename = uri.lastPathSegment + + val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(uri) + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) + downloadManager.enqueue(request) + } + } + + private fun requestDownloadAllMedia(status: Status) { + val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + (activity as BaseActivity).requestPermissions(permissions) { _, grantResults -> + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + downloadAllMedia(status) + } else { + Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show() + } + } + } + + private fun openReportPage(accountId: String, accountUsername: String, statusId: String) { + startActivity(ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId)) + } + + private fun showConfirmDeleteDialog(id: String, position: Int) { + context?.let { + AlertDialog.Builder(it) + .setMessage(R.string.dialog_delete_toot_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteStatus(id) + removeItem(position) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun showConfirmEditDialog(id: String, position: Int, status: Status) { + activity?.let { + AlertDialog.Builder(it) + .setMessage(R.string.dialog_redraft_toot_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteStatus(id) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ deletedStatus -> + removeItem(position) + + val redraftStatus = if (deletedStatus.isEmpty()) { + status.toDeletedStatus() + } else { + deletedStatus + } + + val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions( + tootText = redraftStatus.text ?: "", + inReplyToId = redraftStatus.inReplyToId, + visibility = redraftStatus.visibility, + contentWarning = redraftStatus.spoilerText, + mediaAttachments = redraftStatus.attachments, + sensitive = redraftStatus.sensitive, + poll = redraftStatus.poll?.toNewPoll(status.createdAt) + )) + startActivity(intent) + }, { error -> + Log.w("SearchStatusesFragment", "error deleting status", error) + Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show() + }) + + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + override fun onEmojiReact(react: Boolean, emoji: String, statusId: String) { + viewModel.emojiReact(react, emoji, statusId) + } + + override fun onEmojiReactMenu(view: View, reaction: EmojiReaction, statusId: String) { + val context = requireContext() + val popup = PopupMenu(context, view) + + popup.inflate(R.menu.emoji_reaction_more) + popup.menu.findItem(R.id.emoji_react).isVisible = !reaction.me + popup.menu.findItem(R.id.emoji_unreact).isVisible = reaction.me + + popup.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.emoji_react -> { + onEmojiReact(true, reaction.name, statusId) + return@setOnMenuItemClickListener true + } + R.id.emoji_unreact -> { + onEmojiReact(false, reaction.name, statusId) + return@setOnMenuItemClickListener true + } + R.id.emoji_reacted_by -> { + val intent = newIntent(context, AccountListActivity.Type.REACTED, statusId, reaction.name) + (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) + return@setOnMenuItemClickListener true + } + } + false + } + popup.show() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt new file mode 100644 index 0000000..e1c64e2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt @@ -0,0 +1,31 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.room.* + +@Dao +interface AccountDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(account: AccountEntity): Long + + @Delete + fun delete(account: AccountEntity) + + @Query("SELECT * FROM AccountEntity ORDER BY id ASC") + fun loadAll(): List + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt new file mode 100644 index 0000000..9064aaf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -0,0 +1,90 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.defaultTabs + +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Status + +@Entity(indices = [Index(value = ["domain", "accountId"], + unique = true)]) +@TypeConverters(Converters::class) +data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, + val domain: String, + var accessToken: String, + var isActive: Boolean, + var accountId: String = "", + var username: String = "", + var displayName: String = "", + var profilePictureUrl: String = "", + var notificationsEnabled: Boolean = true, + var notificationsStreamingEnabled: Boolean = true, + var notificationsMentioned: Boolean = true, + var notificationsFollowed: Boolean = true, + var notificationsFollowRequested: Boolean = false, + var notificationsReblogged: Boolean = true, + var notificationsFavorited: Boolean = true, + var notificationsPolls: Boolean = true, + var notificationsEmojiReactions: Boolean = true, + var notificationsChatMessages: Boolean = true, + var notificationsSubscriptions: Boolean = true, + var notificationsMove: Boolean = true, + var notificationSound: Boolean = true, + var notificationVibration: Boolean = true, + var notificationLight: Boolean = true, + var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC, + var defaultMediaSensitivity: Boolean = false, + var alwaysShowSensitiveMedia: Boolean = false, + var alwaysOpenSpoiler: Boolean = false, + var mediaPreviewEnabled: Boolean = true, + var lastNotificationId: String = "0", + var activeNotifications: String = "[]", + var emojis: List = emptyList(), + var tabPreferences: List = defaultTabs(), + var notificationsFilter: String = "[]", + var defaultFormattingSyntax: String = "") { + + val identifier: String + get() = "$domain:$accountId" + + val fullName: String + get() = "@$username@$domain" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AccountEntity + + if (id == other.id) return true + if (domain == other.domain && accountId == other.accountId) return true + + return false + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + domain.hashCode() + result = 31 * result + accountId.hashCode() + return result + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt new file mode 100644 index 0000000..2e532c2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -0,0 +1,203 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import android.util.Log +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Status +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.Comparator + +/** + * This class caches the account database and handles all account related operations + * @author ConnyDuck + */ + +private const val TAG = "AccountManager" + +@Singleton +class AccountManager @Inject constructor(db: AppDatabase) { + + @Volatile + var activeAccount: AccountEntity? = null + + var accounts: MutableList = mutableListOf() + private set + private val accountDao: AccountDao = db.accountDao() + + init { + accounts = accountDao.loadAll().toMutableList() + + activeAccount = accounts.find { acc -> + acc.isActive + } + } + + /** + * Adds a new empty account and makes it the active account. + * More account information has to be added later with [updateActiveAccount] + * or the account wont be saved to the database. + * @param accessToken the access token for the new account + * @param domain the domain of the accounts Mastodon instance + */ + fun addAccount(accessToken: String, domain: String) { + + activeAccount?.let { + it.isActive = false + Log.d(TAG, "addAccount: saving account with id " + it.id) + + accountDao.insertOrReplace(it) + } + + val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 + val newAccountId = maxAccountId + 1 + activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true) + + } + + /** + * Saves an already known account to the database. + * New accounts must be created with [addAccount] + * @param account the account to save + */ + fun saveAccount(account: AccountEntity) { + if (account.id != 0L) { + Log.d(TAG, "saveAccount: saving account with id " + account.id) + accountDao.insertOrReplace(account) + } + + } + + /** + * Logs the current account out by deleting all data of the account. + * @return the new active account, or null if no other account was found + */ + fun logActiveAccountOut(): AccountEntity? { + + if (activeAccount == null) { + return null + } else { + accounts.remove(activeAccount!!) + accountDao.delete(activeAccount!!) + + if (accounts.size > 0) { + accounts[0].isActive = true + activeAccount = accounts[0] + Log.d(TAG, "logActiveAccountOut: saving account with id " + accounts[0].id) + accountDao.insertOrReplace(accounts[0]) + } else { + activeAccount = null + } + return activeAccount + + } + + } + + /** + * updates the current account with new information from the mastodon api + * and saves it in the database + * @param account the [Account] object returned from the api + */ + fun updateActiveAccount(account: Account) { + activeAccount?.let { + it.accountId = account.id + it.username = account.username + it.displayName = account.name + it.profilePictureUrl = account.avatar + it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC + it.defaultMediaSensitivity = account.source?.sensitive ?: false + it.emojis = account.emojis ?: emptyList() + + Log.d(TAG, "updateActiveAccount: saving account with id " + it.id) + it.id = accountDao.insertOrReplace(it) + + val accountIndex = accounts.indexOf(it) + + if (accountIndex != -1) { + //in case the user was already logged in with this account, remove the old information + accounts.removeAt(accountIndex) + accounts.add(accountIndex, it) + } else { + accounts.add(it) + } + + } + } + + /** + * changes the active account + * @param accountId the database id of the new active account + */ + fun setActiveAccount(accountId: Long) { + + activeAccount?.let { + Log.d(TAG, "setActiveAccount: saving account with id " + it.id) + it.isActive = false + saveAccount(it) + } + + activeAccount = accounts.find { (id) -> + id == accountId + } + + activeAccount?.let { + it.isActive = true + accountDao.insertOrReplace(it) + } + } + + /** + * @return an immutable list of all accounts in the database with the active account first + */ + fun getAllAccountsOrderedByActive(): List { + val accountsCopy = accounts.toMutableList() + accountsCopy.sortWith(Comparator { l, r -> + when { + l.isActive && !r.isActive -> -1 + r.isActive && !l.isActive -> 1 + else -> 0 + } + }) + + return accountsCopy + } + + /** + * @return true if at least one account has notifications enabled + */ + fun areNotificationsEnabled(): Boolean { + return accounts.any { it.notificationsEnabled } + } + + fun areNotificationsStreamingEnabled() : Boolean { + return accounts.any { it.notificationsStreamingEnabled } + } + + /** + * Finds an account by its database id + * @param accountId the id of the account + * @return the requested account or null if it was not found + */ + fun getAccountById(accountId: Long): AccountEntity? { + return accounts.find { (id) -> + id == accountId + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java new file mode 100644 index 0000000..d36130d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -0,0 +1,408 @@ +/* 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.db; + +import androidx.annotation.NonNull; +import androidx.room.Database; +import androidx.room.RoomDatabase; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import com.keylesspalace.tusky.TabDataKt; +import com.keylesspalace.tusky.components.conversation.ConversationEntity; + +/** + * DB version & declare DAO + */ + +@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, + TimelineAccountEntity.class, ConversationEntity.class, ChatEntity.class, ChatMessageEntity.class + }, version = 27) +public abstract class AppDatabase extends RoomDatabase { + + public abstract TootDao tootDao(); + public abstract AccountDao accountDao(); + public abstract InstanceDao instanceDao(); + public abstract ConversationsDao conversationDao(); + public abstract TimelineDao timelineDao(); + public abstract ChatsDao chatsDao(); + public abstract DraftDao draftDao(); + + public static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT, urls TEXT, contentWarning TEXT);"); + database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); + database.execSQL("DROP TABLE TootEntity;"); + database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); + } + }; + + public static final Migration MIGRATION_3_4 = new Migration(3, 4) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER"); + } + }; + + public static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `AccountEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, " + + "`isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, `displayName` TEXT NOT NULL, " + + "`profilePictureUrl` TEXT NOT NULL, " + + "`notificationsEnabled` INTEGER NOT NULL, " + + "`notificationsMentioned` INTEGER NOT NULL, " + + "`notificationsFollowed` INTEGER NOT NULL, " + + "`notificationsReblogged` INTEGER NOT NULL, " + + "`notificationsFavorited` INTEGER NOT NULL, " + + "`notificationSound` INTEGER NOT NULL, " + + "`notificationVibration` INTEGER NOT NULL, " + + "`notificationLight` INTEGER NOT NULL, " + + "`lastNotificationId` TEXT NOT NULL, " + + "`activeNotifications` TEXT NOT NULL)"); + database.execSQL("CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)"); + } + }; + + public static final Migration MIGRATION_5_6 = new Migration(5, 6) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))"); + } + }; + + public static final Migration MIGRATION_6_7 = new Migration(6, 7) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))"); + database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`, NULL FROM `EmojiListEntity`;"); + database.execSQL("DROP TABLE `EmojiListEntity`;"); + } + }; + + public static final Migration MIGRATION_7_8 = new Migration(7, 8) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `emojis` TEXT NOT NULL DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_8_9 = new Migration(8, 9) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `descriptions` TEXT DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_9_10 = new Migration(9, 10) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostPrivacy` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultMediaSensitivity` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysShowSensitiveMedia` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `mediaPreviewEnabled` INTEGER NOT NULL DEFAULT '1'"); + } + }; + + public static final Migration MIGRATION_10_11 = new Migration(10, 11) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`instance` TEXT NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`url` TEXT, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`authorServerId` TEXT," + + "`instance` TEXT, " + + "`inReplyToId` TEXT, " + + "`inReplyToAccountId` TEXT, " + + "`content` TEXT, " + + "`createdAt` INTEGER NOT NULL, " + + "`emojis` TEXT, " + + "`reblogsCount` INTEGER NOT NULL, " + + "`favouritesCount` INTEGER NOT NULL, " + + "`reblogged` INTEGER NOT NULL, " + + "`favourited` INTEGER NOT NULL, " + + "`sensitive` INTEGER NOT NULL, " + + "`spoilerText` TEXT, " + + "`visibility` INTEGER, " + + "`attachments` TEXT, " + + "`mentions` TEXT, " + + "`application` TEXT, " + + "`reblogServerId` TEXT, " + + "`reblogAccountId` TEXT," + + " PRIMARY KEY(`serverId`, `timelineUserId`)," + + " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + + "ON UPDATE NO ACTION ON DELETE NO ACTION )"); + database.execSQL("CREATE INDEX IF NOT EXISTS" + + "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + + "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_11_12 = new Migration(11, 12) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + String defaultTabs = TabDataKt.HOME + ";" + + TabDataKt.NOTIFICATIONS + ";" + + TabDataKt.LOCAL + ";" + + TabDataKt.FEDERATED; + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `tabPreferences` TEXT NOT NULL DEFAULT '" + defaultTabs + "'"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL, " + + "`id` TEXT NOT NULL, " + + "`accounts` TEXT NOT NULL, " + + "`unread` INTEGER NOT NULL, " + + "`s_id` TEXT NOT NULL, " + + "`s_url` TEXT, " + + "`s_inReplyToId` TEXT, " + + "`s_inReplyToAccountId` TEXT, " + + "`s_account` TEXT NOT NULL, " + + "`s_content` TEXT NOT NULL, " + + "`s_createdAt` INTEGER NOT NULL, " + + "`s_emojis` TEXT NOT NULL, " + + "`s_favouritesCount` INTEGER NOT NULL, " + + "`s_favourited` INTEGER NOT NULL, " + + "`s_sensitive` INTEGER NOT NULL, " + + "`s_spoilerText` TEXT NOT NULL, " + + "`s_attachments` TEXT NOT NULL, " + + "`s_mentions` TEXT NOT NULL, " + + "`s_showingHiddenContent` INTEGER NOT NULL, " + + "`s_expanded` INTEGER NOT NULL, " + + "`s_collapsible` INTEGER NOT NULL, " + + "`s_collapsed` INTEGER NOT NULL, " + + "PRIMARY KEY(`id`, `accountId`))"); + + } + }; + + public static final Migration MIGRATION_12_13 = new Migration(12, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + database.execSQL("DROP TABLE IF EXISTS `TimelineAccountEntity`"); + database.execSQL("DROP TABLE IF EXISTS `TimelineStatusEntity`"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`url` TEXT, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`authorServerId` TEXT," + + "`inReplyToId` TEXT, " + + "`inReplyToAccountId` TEXT, " + + "`content` TEXT, " + + "`createdAt` INTEGER NOT NULL, " + + "`emojis` TEXT, " + + "`reblogsCount` INTEGER NOT NULL, " + + "`favouritesCount` INTEGER NOT NULL, " + + "`reblogged` INTEGER NOT NULL, " + + "`favourited` INTEGER NOT NULL, " + + "`sensitive` INTEGER NOT NULL, " + + "`spoilerText` TEXT, " + + "`visibility` INTEGER, " + + "`attachments` TEXT, " + + "`mentions` TEXT, " + + "`application` TEXT, " + + "`reblogServerId` TEXT, " + + "`reblogAccountId` TEXT," + + " PRIMARY KEY(`serverId`, `timelineUserId`)," + + " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + + "ON UPDATE NO ACTION ON DELETE NO ACTION )"); + database.execSQL("CREATE INDEX IF NOT EXISTS" + + "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + + "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_10_13 = new Migration(10, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + MIGRATION_11_12.migrate(database); + MIGRATION_12_13.migrate(database); + } + }; + + public static final Migration MIGRATION_13_14 = new Migration(13, 14) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT NULL DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_14_15 = new Migration(14, 15) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `poll` TEXT"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_poll` TEXT"); + } + }; + + public static final Migration MIGRATION_15_16 = new Migration(15, 16) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsPolls` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_16_17 = new Migration(16, 17) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineAccountEntity` ADD COLUMN `bot` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_17_18 = new Migration(17, 18) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysOpenSpoiler` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_18_19 = new Migration(18, 19) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptions` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptionLength` INTEGER"); + + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `poll` TEXT"); + } + }; + + public static final Migration MIGRATION_19_20 = new Migration(19, 20) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT NULL DEFAULT 0"); + } + + }; + + public static final Migration MIGRATION_20_21 = new Migration(20, 21) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `version` TEXT"); + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `markdownMode` INTEGER"); + } + }; + + public static final Migration MIGRATION_21_22 = new Migration(21, 22) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsEmojiReactions` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_22_23 = new Migration(22, 23) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // leave markdownMode unused, we don't need it anymore but don't recreate table + // database.execSQL("ALTER TABLE `TootEntity` DROP COLUMN `markdownMode`"); + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `formattingSyntax` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultFormattingSyntax` TEXT NOT NULL DEFAULT ''"); + } + }; + + public static final Migration MIGRATION_23_24 = new Migration(23, 24) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_24_25 = new Migration(24, 25) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `ChatEntity` (`localId` INTEGER NOT NULL," + + "`chatId` TEXT NOT NULL," + + "`accountId` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`updatedAt` INTEGER NOT NULL," + + "`lastMessageId` TEXT," + + "PRIMARY KEY (`localId`, `chatId`))"); + database.execSQL("CREATE TABLE `ChatMessageEntity` (`localId` INTEGER NOT NULL," + + "`messageId` TEXT NOT NULL," + + "`content` TEXT," + + "`chatId` TEXT NOT NULL," + + "`accountId` TEXT NOT NULL," + + "`createdAt` INTEGER NOT NULL," + + "`attachment` TEXT," + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY (`localId`, `messageId`))"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsStreamingEnabled` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `pleroma` TEXT"); + } + }; + + public static final Migration MIGRATION_25_26 = new Migration(25, 26) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsMove` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_26_27 = new Migration(26, 27) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`inReplyToId` TEXT," + + "`content` TEXT," + + "`contentWarning` TEXT," + + "`sensitive` INTEGER NOT NULL," + + "`visibility` INTEGER NOT NULL," + + "`attachments` TEXT NOT NULL," + + "`poll` TEXT," + + "`formattingSyntax` TEXT NOT NULL," + + "`failedToSend` INTEGER NOT NULL)" + ); + } + }; +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ChatEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/ChatEntity.kt new file mode 100644 index 0000000..1bba2ef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/ChatEntity.kt @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky.db + +import androidx.room.* + +@Entity( + primaryKeys = ["localId", "chatId"] +) +data class ChatEntity ( + val localId: Long, /* our user account id */ + val chatId: String, + val accountId: String, + val unread: Long, + val updatedAt: Long, + val lastMessageId: String? +) + +data class ChatEntityWithAccount ( + @Embedded val chat: ChatEntity, + @Embedded(prefix = "a_") val account: TimelineAccountEntity?, + @Embedded(prefix = "msg_") val lastMessage: ChatMessageEntity? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ChatMessageEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/ChatMessageEntity.kt new file mode 100644 index 0000000..9b43d66 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/ChatMessageEntity.kt @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky.db + +import androidx.room.Entity + +/* + * ChatMessage model + */ + +@Entity( + primaryKeys = ["localId", "messageId"] +) +data class ChatMessageEntity( + val localId: Long, + val messageId: String, + val content: String?, + val chatId: String, + val accountId: String, + val createdAt: Long, + val attachment: String?, + val emojis: String +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt new file mode 100644 index 0000000..8b0e7fc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/ChatsDao.kt @@ -0,0 +1,84 @@ +package com.keylesspalace.tusky.db + +import androidx.room.* +import androidx.room.OnConflictStrategy.IGNORE +import androidx.room.OnConflictStrategy.REPLACE +import io.reactivex.Single + +@Dao +abstract class ChatsDao { + + @Query("""SELECT c.chatId, c.localId, c.accountId, c.lastMessageId, c.unread, c.updatedAt, +a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', +a.localUsername as 'a_localUsername', a.username as 'a_username', +a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', +a.emojis as 'a_emojis', a.bot as 'a_bot', +msg.accountId as 'msg_accountId', msg.localId as 'msg_localId', +msg.chatId as 'msg_chatId', msg.attachment as 'msg_attachment', +msg.content as 'msg_content', msg.createdAt as 'msg_createdAt', msg.emojis as 'msg_emojis', +msg.messageId as 'msg_messageId' +FROM ChatEntity c +LEFT JOIN TimelineAccountEntity a ON (a.timelineUserId == :localId AND a.serverId = c.accountId) +LEFT JOIN ChatMessageEntity msg ON (msg.localId == :localId AND msg.chatId == c.chatId) +WHERE c.localId = :localId +AND (CASE WHEN :maxId IS NOT NULL THEN +(LENGTH(c.chatId) < LENGTH(:maxId) OR LENGTH(c.chatId) == LENGTH(:maxId) AND c.chatId < :maxId) +ELSE 1 END) +AND (CASE WHEN :sinceId IS NOT NULL THEN +(LENGTH(c.chatId) > LENGTH(:sinceId) OR LENGTH(c.chatId) == LENGTH(:sinceId) AND c.chatId > :sinceId) +ELSE 1 END) +ORDER BY c.updatedAt DESC +LIMIT :limit + """) + abstract fun getChatsForAccount(localId: Long, maxId: String?, sinceId: String?, limit: Int) : Single> + + @Insert(onConflict = REPLACE) + abstract fun insertChat(chatEntity: ChatEntity) : Long + + @Insert(onConflict = IGNORE) + abstract fun insertChatIfNotThere(chatEntity: ChatEntity): Long + + @Insert(onConflict = REPLACE) + abstract fun insertAccount(accountEntity: TimelineAccountEntity) : Long + + @Insert(onConflict = REPLACE) + abstract fun insertChatMessage(chatMessageEntity: ChatMessageEntity) : Long + + @Transaction + open fun insertInTransaction(chatEntity: ChatEntity, lastMessage: ChatMessageEntity?, accountEntity: TimelineAccountEntity) { + insertAccount(accountEntity) + lastMessage?.let(this::insertChatMessage) + insertChat(chatEntity) + } + + @Transaction + open fun setLastMessage(accountId: Long, chatId: String, lastMessageEntity: ChatMessageEntity) { + insertChatMessage(lastMessageEntity) + setLastMessageId(accountId, chatId, lastMessageEntity.messageId) + } + + @Query("""UPDATE ChatEntity SET lastMessageId = :messageId WHERE localId = :localId AND chatId = :chatId""") + abstract fun setLastMessageId(localId: Long, chatId: String, messageId: String) + + @Query("""DELETE FROM ChatEntity WHERE accountId = "" +AND localId = :account AND +(LENGTH(chatId) < LENGTH(:maxId) OR LENGTH(chatId) == LENGTH(:maxId) AND chatId < :maxId) +AND +(LENGTH(chatId) > LENGTH(:sinceId) OR LENGTH(chatId) == LENGTH(:sinceId) AND chatId > :sinceId) +""") + abstract fun removeAllPlaceholdersBetween(account: Long, maxId: String, sinceId: String) + + @Query("""DELETE FROM ChatEntity WHERE localId = :accountId AND +(LENGTH(chatId) < LENGTH(:maxId) OR LENGTH(chatId) == LENGTH(:maxId) AND chatId < :maxId) +AND +(LENGTH(chatId) > LENGTH(:minId) OR LENGTH(chatId) == LENGTH(:minId) AND chatId > :minId) + """) + abstract fun deleteRange(accountId: Long, minId: String, maxId: String) + + + @Query("""DELETE FROM ChatEntity WHERE localId = :localId AND accountId = :accountId""") + abstract fun deleteChatByAccount(localId: Long, accountId: String) + + @Query("""DELETE FROM ChatEntity WHERE localId = :localId AND chatId = :chatId""") + abstract fun deleteChat(localId: Long, chatId: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt new file mode 100644 index 0000000..00f32f5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -0,0 +1,41 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.paging.DataSource +import androidx.room.* +import com.keylesspalace.tusky.components.conversation.ConversationEntity +import io.reactivex.Single + +@Dao +interface ConversationsDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(conversations: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(conversation: ConversationEntity): Single + + @Delete + fun delete(conversation: ConversationEntity): Single + + @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") + fun conversationsForAccount(accountId: Long) : DataSource.Factory + + @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") + fun deleteForAccount(accountId: Long) + + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt new file mode 100644 index 0000000..1b1f94f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -0,0 +1,170 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import android.text.Spanned +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml +import androidx.room.TypeConverter +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity +import com.keylesspalace.tusky.createTabDataFromId +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.json.SpannedTypeAdapter +import com.keylesspalace.tusky.util.trimTrailingWhitespace +import java.net.URLDecoder +import java.net.URLEncoder +import java.util.* + +class Converters { + + private val gson = GsonBuilder() + .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) + .create() + + @TypeConverter + fun jsonToEmojiList(emojiListJson: String?): List? { + return gson.fromJson(emojiListJson, object : TypeToken>() {}.type) + } + + @TypeConverter + fun emojiListToJson(emojiList: List?): String { + return gson.toJson(emojiList) + } + + @TypeConverter + fun visibilityToInt(visibility: Status.Visibility?): Int { + return visibility?.num ?: Status.Visibility.UNKNOWN.num + } + + @TypeConverter + fun intToVisibility(visibility: Int): Status.Visibility { + return Status.Visibility.byNum(visibility) + } + + @TypeConverter + fun stringToTabData(str: String?): List? { + return str?.split(";") + ?.map { + val data = it.split(":") + createTabDataFromId(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }) + } + } + + @TypeConverter + fun tabDataToString(tabData: List?): String? { + // List name may include ":" + return tabData?.joinToString(";") { it.id + ":" + it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") } } + } + + @TypeConverter + fun accountToJson(account: ConversationAccountEntity?): String { + return gson.toJson(account) + } + + @TypeConverter + fun jsonToAccount(accountJson: String?): ConversationAccountEntity? { + return gson.fromJson(accountJson, ConversationAccountEntity::class.java) + } + + @TypeConverter + fun accountListToJson(accountList: List?): String { + return gson.toJson(accountList) + } + + @TypeConverter + fun jsonToAccountList(accountListJson: String?): List? { + return gson.fromJson(accountListJson, object : TypeToken>() {}.type) + } + + @TypeConverter + fun attachmentListToJson(attachmentList: List?): String { + return gson.toJson(attachmentList) + } + + @TypeConverter + fun jsonToAttachmentList(attachmentListJson: String?): ArrayList? { + return gson.fromJson(attachmentListJson, object : TypeToken>() {}.type) + } + + @TypeConverter + fun mentionArrayToJson(mentionArray: Array?): String? { + return gson.toJson(mentionArray) + } + + @TypeConverter + fun jsonToMentionArray(mentionListJson: String?): Array? { + return gson.fromJson(mentionListJson, object : TypeToken>() {}.type) + } + + @TypeConverter + fun dateToLong(date: Date): Long { + return date.time + } + + @TypeConverter + fun longToDate(date: Long): Date { + return Date(date) + } + + @TypeConverter + fun spannedToString(spanned: Spanned?): String? { + if(spanned == null) { + return null + } + return spanned.toHtml() + } + + @TypeConverter + fun stringToSpanned(spannedString: String?): Spanned? { + if(spannedString == null) { + return null + } + return spannedString.parseAsHtml().trimTrailingWhitespace() + } + + @TypeConverter + fun pollToJson(poll: Poll?): String? { + return gson.toJson(poll) + } + + @TypeConverter + fun jsonToPoll(pollJson: String?): Poll? { + return gson.fromJson(pollJson, Poll::class.java) + } + + @TypeConverter + fun newPollToJson(newPoll: NewPoll?): String? { + return gson.toJson(newPoll) + } + + @TypeConverter + fun jsonToNewPoll(newPollJson: String?): NewPoll? { + return gson.fromJson(newPollJson, NewPoll::class.java) + } + + @TypeConverter + fun draftAttachmentListToJson(draftAttachments: List?): String? { + return gson.toJson(draftAttachments) + } + + @TypeConverter + fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List? { + return gson.fromJson(draftAttachmentListJson, object : TypeToken>() {}.type) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt new file mode 100644 index 0000000..105fd7c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.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.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Completable +import io.reactivex.Single + +@Dao +interface DraftDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(draft: DraftEntity): Completable + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") + fun loadDrafts(accountId: Long): DataSource.Factory + + @Query("DELETE FROM DraftEntity WHERE id = :id") + fun delete(id: Int): Completable + + @Query("SELECT * FROM DraftEntity WHERE id = :id") + fun find(id: Int): Single +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt new file mode 100644 index 0000000..9f06740 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -0,0 +1,56 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import kotlinx.android.parcel.Parcelize + +@Entity +@TypeConverters(Converters::class) +data class DraftEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val accountId: Long, + val inReplyToId: String?, + val content: String?, + val contentWarning: String?, + val sensitive: Boolean, + val visibility: Status.Visibility, + val attachments: List, + val poll: NewPoll?, + val formattingSyntax: String, + val failedToSend: Boolean +) + +@Parcelize +data class DraftAttachment( + val uriString: String, + val description: String?, + val type: Type +): Parcelable { + val uri: Uri + get() = uriString.toUri() + + enum class Type { + IMAGE, VIDEO, AUDIO, UNKNOWN; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt new file mode 100644 index 0000000..0c78349 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -0,0 +1,31 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Single + +@Dao +interface InstanceDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(instance: InstanceEntity) + + @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") + fun loadMetadataForInstance(instance: String): Single +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt new file mode 100644 index 0000000..0d90c29 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -0,0 +1,33 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.entity.Emoji + +@Entity +@TypeConverters(Converters::class) +data class InstanceEntity( + @field:PrimaryKey var instance: String, + val emojiList: List?, + val maximumTootCharacters: Int?, + val maxPollOptions: Int?, + val maxPollOptionLength: Int?, + val version: String?, + val chatLimit: Int? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt new file mode 100644 index 0000000..1c9f4e1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -0,0 +1,111 @@ +package com.keylesspalace.tusky.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.IGNORE +import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.Single + +@Dao +abstract class TimelineDao { + + @Insert(onConflict = REPLACE) + abstract fun insertAccount(timelineAccountEntity: TimelineAccountEntity): Long + + @Insert(onConflict = REPLACE) + abstract fun insertStatus(timelineAccountEntity: TimelineStatusEntity): Long + + + @Insert(onConflict = IGNORE) + abstract fun insertStatusIfNotThere(timelineAccountEntity: TimelineStatusEntity): Long + + @Query(""" +SELECT s.serverId, s.url, s.timelineUserId, +s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, +s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, +s.spoilerText, s.visibility, s.mentions, s.application, s.reblogServerId,s.reblogAccountId, +s.content, s.attachments, s.poll, s.pleroma, +a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', +a.localUsername as 'a_localUsername', a.username as 'a_username', +a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', +a.emojis as 'a_emojis', a.bot as 'a_bot', +rb.serverId as 'rb_serverId', rb.timelineUserId 'rb_timelineUserId', +rb.localUsername as 'rb_localUsername', rb.username as 'rb_username', +rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar', +rb.emojis as'rb_emojis', rb.bot as 'rb_bot' +FROM TimelineStatusEntity s +LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId) +LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId) +WHERE s.timelineUserId = :account +AND (CASE WHEN :maxId IS NOT NULL THEN +(LENGTH(s.serverId) < LENGTH(:maxId) OR LENGTH(s.serverId) == LENGTH(:maxId) AND s.serverId < :maxId) +ELSE 1 END) +AND (CASE WHEN :sinceId IS NOT NULL THEN +(LENGTH(s.serverId) > LENGTH(:sinceId) OR LENGTH(s.serverId) == LENGTH(:sinceId) AND s.serverId > :sinceId) +ELSE 1 END) +ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC +LIMIT :limit""") + abstract fun getStatusesForAccount(account: Long, maxId: String?, sinceId: String?, limit: Int): Single> + + @Transaction + open fun insertInTransaction(status: TimelineStatusEntity, account: TimelineAccountEntity, + reblogAccount: TimelineAccountEntity?) { + insertAccount(account) + reblogAccount?.let(this::insertAccount) + insertStatus(status) + } + + @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND + (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId) +AND +(LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId > :minId) + """) + abstract fun deleteRange(accountId: Long, minId: String, maxId: String) + + @Query("""DELETE FROM TimelineStatusEntity WHERE authorServerId = null +AND timelineUserId = :account AND +(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId) +AND +(LENGTH(serverId) > LENGTH(:sinceId) OR LENGTH(serverId) == LENGTH(:sinceId) AND serverId > :sinceId) +""") + abstract fun removeAllPlaceholdersBetween(account: Long, maxId: String, sinceId: String) + + @Query("""UPDATE TimelineStatusEntity SET favourited = :favourited +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + abstract fun setFavourited(accountId: Long, statusId: String, favourited: Boolean) + + @Query("""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + abstract fun setBookmarked(accountId: Long, statusId: String, bookmarked: Boolean) + + @Query("""UPDATE TimelineStatusEntity SET reblogged = :reblogged +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + abstract fun setReblogged(accountId: Long, statusId: String, reblogged: Boolean) + + @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND +(authorServerId = :userId OR reblogAccountId = :userId)""") + abstract fun removeAllByUser(accountId: Long, userId: String) + + @Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId") + abstract fun removeAllForAccount(accountId: Long) + + @Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId") + abstract fun removeAllUsersForAccount(accountId: Long) + + @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId +AND serverId = :statusId""") + abstract fun delete(accountId: Long, statusId: String) + + @Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""") + abstract fun cleanup(olderThan: Long) + + @Query("""UPDATE TimelineStatusEntity SET poll = :poll +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + abstract fun setVoted(accountId: Long, statusId: String, poll: String) + + @Query("""UPDATE TimelineStatusEntity SET pleroma = :pleroma +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + abstract fun setPleroma(accountId: Long, statusId: String, pleroma: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt new file mode 100644 index 0000000..4893708 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -0,0 +1,81 @@ +package com.keylesspalace.tusky.db + +import androidx.room.* +import com.keylesspalace.tusky.entity.Status + +/** + * We're trying to play smart here. Server sends us reblogs as two entities one embedded into + * another (reblogged status is a field inside of "reblog" status). But it's really inefficient from + * the DB perspective and doesn't matter much for the display/interaction purposes. + * What if when we store reblog we don't store almost empty "reblog status" but we store + * *reblogged* status and we embed "reblog status" into reblogged status. This reversed + * relationship takes much less space and is much faster to fetch (no N+1 type queries or JSON + * serialization). + * "Reblog status", if present, is marked by [reblogServerId], and [reblogAccountId] + * fields. + */ +@Entity( + primaryKeys = ["serverId", "timelineUserId"], + foreignKeys = ([ + ForeignKey( + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "timelineUserId"], + childColumns = ["authorServerId", "timelineUserId"] + ) + ]), + // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). + indices = [Index("authorServerId", "timelineUserId")] +) +@TypeConverters(Converters::class) +data class TimelineStatusEntity( + val serverId: String, // id never flips: we need it for sorting so it's a real id + val url: String?, + // our local id for the logged in user in case there are multiple accounts per instance + val timelineUserId: Long, + val authorServerId: String?, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val content: String?, + val createdAt: Long, + val emojis: String?, + val reblogsCount: Int, + val favouritesCount: Int, + val reblogged: Boolean, + val bookmarked: Boolean, + val favourited: Boolean, + val sensitive: Boolean, + val spoilerText: String?, + val visibility: Status.Visibility?, + val attachments: String?, + val mentions: String?, + val application: String?, + val reblogServerId: String?, // if it has a reblogged status, it's id is stored here + val reblogAccountId: String?, + val poll: String?, + val pleroma: String? +) + +@Entity( + primaryKeys = ["serverId", "timelineUserId"] +) +data class TimelineAccountEntity( + val serverId: String, + val timelineUserId: Long, + val localUsername: String, + val username: String, + val displayName: String, + val url: String, + val avatar: String, + val emojis: String, + val bot: Boolean +) + + +class TimelineStatusWithAccount { + @Embedded + lateinit var status: TimelineStatusEntity + @Embedded(prefix = "a_") + lateinit var account: TimelineAccountEntity + @Embedded(prefix = "rb_") + var reblogAccount: TimelineAccountEntity? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java new file mode 100644 index 0000000..f46c275 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java @@ -0,0 +1,45 @@ +/* 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.db; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import io.reactivex.Observable; + +/** + * Created by cto3543 on 28/06/2017. + * + * DAO to fetch and update toots in the DB. + */ + +@Dao +public interface TootDao { + + @Query("SELECT * FROM TootEntity ORDER BY uid DESC") + List loadAll(); + + @Query("DELETE FROM TootEntity WHERE uid = :uid") + int delete(int uid); + + @Query("SELECT * FROM TootEntity WHERE uid = :uid") + TootEntity find(int uid); + + @Query("SELECT COUNT(*) FROM TootEntity") + Observable savedTootCount(); +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java b/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java new file mode 100644 index 0000000..d40bcc7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java @@ -0,0 +1,170 @@ +/* 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.db; + +import com.google.gson.Gson; +import com.keylesspalace.tusky.entity.NewPoll; +import com.keylesspalace.tusky.entity.Status; + +import androidx.annotation.*; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; +import androidx.room.TypeConverter; +import androidx.room.TypeConverters; + +/** + * Toot model. + */ + +@Entity +@TypeConverters(TootEntity.Converters.class) +public class TootEntity { + @PrimaryKey(autoGenerate = true) + private final int uid; + + @ColumnInfo(name = "text") + private final String text; + + @ColumnInfo(name = "urls") + private final String urls; + + @ColumnInfo(name = "descriptions") + private final String descriptions; + + @ColumnInfo(name = "contentWarning") + private final String contentWarning; + + @ColumnInfo(name = "inReplyToId") + private final String inReplyToId; + + @Nullable + @ColumnInfo(name = "inReplyToText") + private final String inReplyToText; + + @Nullable + @ColumnInfo(name = "inReplyToUsername") + private final String inReplyToUsername; + + @ColumnInfo(name = "visibility") + private final Status.Visibility visibility; + + @Nullable + @ColumnInfo(name = "poll") + private final NewPoll poll; + + @NonNull + @ColumnInfo(name = "formattingSyntax") + private final String formattingSyntax; + + /* DEPRECATED */ + @Nullable + @ColumnInfo(name = "markdownMode") + public Boolean markdownMode = false; + + public TootEntity(int uid, String text, String urls, String descriptions, String contentWarning, String inReplyToId, + @Nullable String inReplyToText, @Nullable String inReplyToUsername, + Status.Visibility visibility, @Nullable NewPoll poll, String formattingSyntax) { + this.uid = uid; + this.text = text; + this.urls = urls; + this.descriptions = descriptions; + this.contentWarning = contentWarning; + this.inReplyToId = inReplyToId; + this.inReplyToText = inReplyToText; + this.inReplyToUsername = inReplyToUsername; + this.visibility = visibility; + this.poll = poll; + this.formattingSyntax = formattingSyntax; + } + + public String getText() { + return text; + } + + public String getContentWarning() { + return contentWarning; + } + + public int getUid() { + return uid; + } + + public String getUrls() { + return urls; + } + + public String getDescriptions() { + return descriptions; + } + + public String getInReplyToId() { + return inReplyToId; + } + + @Nullable + public String getInReplyToText() { + return inReplyToText; + } + + @Nullable + public String getInReplyToUsername() { + return inReplyToUsername; + } + + public Status.Visibility getVisibility() { + return visibility; + } + + @Nullable + public NewPoll getPoll() { + return poll; + } + + public String getFormattingSyntax() { + return formattingSyntax; + } + + @Nullable + public Boolean getMarkdownMode() { + return markdownMode; + } + + public static final class Converters { + + private static final Gson gson = new Gson(); + + @TypeConverter + public Status.Visibility visibilityFromInt(int number) { + return Status.Visibility.byNum(number); + } + + @TypeConverter + public int intFromVisibility(Status.Visibility visibility) { + return visibility == null ? Status.Visibility.UNKNOWN.getNum() : visibility.getNum(); + } + + @TypeConverter + public String pollToString(NewPoll poll) { + return gson.toJson(poll); + } + + @TypeConverter + public NewPoll stringToPoll(String poll) { + return gson.fromJson(poll, NewPoll.class); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt new file mode 100644 index 0000000..47227cc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -0,0 +1,118 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.components.chat.ChatActivity +import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.drafts.DraftsActivity +import com.keylesspalace.tusky.components.instancemute.InstanceListActivity +import com.keylesspalace.tusky.components.preference.PreferencesActivity +import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity +import com.keylesspalace.tusky.components.search.SearchActivity +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Created by charlag on 3/24/18. + */ + +@Module +abstract class ActivitiesModule { + + @ContributesAndroidInjector + abstract fun contributesBaseActivity(): BaseActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesMainActivity(): MainActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesAccountActivity(): AccountActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesListsActivity(): ListsActivity + + @ContributesAndroidInjector + abstract fun contributesComposeActivity(): ComposeActivity + + @ContributesAndroidInjector + abstract fun contributesChatActivity(): ChatActivity + + @ContributesAndroidInjector + abstract fun contributesEditProfileActivity(): EditProfileActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesAccountListActivity(): AccountListActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesModalTimelineActivity(): ModalTimelineActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesViewTagActivity(): ViewTagActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesViewThreadActivity(): ViewThreadActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesStatusListActivity(): StatusListActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesSearchAvtivity(): SearchActivity + + @ContributesAndroidInjector + abstract fun contributesAboutActivity(): AboutActivity + + @ContributesAndroidInjector + abstract fun contributesLoginActivity(): LoginActivity + + @ContributesAndroidInjector + abstract fun contributesSplashActivity(): SplashActivity + + @ContributesAndroidInjector + abstract fun contributesSavedTootActivity(): SavedTootActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesPreferencesActivity(): PreferencesActivity + + @ContributesAndroidInjector + abstract fun contributesViewMediaActivity(): ViewMediaActivity + + @ContributesAndroidInjector + abstract fun contributesLicenseActivity(): LicenseActivity + + @ContributesAndroidInjector + abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity + + @ContributesAndroidInjector + abstract fun contributesFiltersActivity(): FiltersActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesReportActivity(): ReportActivity + + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) + abstract fun contributesInstanceListActivity(): InstanceListActivity + + @ContributesAndroidInjector + abstract fun contributesScheduledTootActivity(): ScheduledTootActivity + + @ContributesAndroidInjector + abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity + + @ContributesAndroidInjector + abstract fun contributesDraftActivity(): DraftsActivity +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt new file mode 100644 index 0000000..c18362b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt @@ -0,0 +1,52 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import com.keylesspalace.tusky.TuskyApplication +import dagger.BindsInstance +import dagger.Component +import dagger.android.support.AndroidSupportInjectionModule +import javax.inject.Singleton + + +/** + * Created by charlag on 3/21/18. + */ + +@Singleton +@Component(modules = [ + AppModule::class, + NetworkModule::class, + AndroidSupportInjectionModule::class, + ActivitiesModule::class, + ServicesModule::class, + BroadcastReceiverModule::class, + ViewModelModule::class, + RepositoryModule::class, + MediaUploaderModule::class, + GlideModule::class +]) +interface AppComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun application(tuskyApp: TuskyApplication): Builder + + fun build(): AppComponent + } + + fun inject(app: TuskyApplication) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt new file mode 100644 index 0000000..bd06bfc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt @@ -0,0 +1,80 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import com.keylesspalace.tusky.TuskyApplication +import dagger.android.AndroidInjection +import dagger.android.HasAndroidInjector +import dagger.android.support.AndroidSupportInjection + +/** + * Created by charlag on 3/24/18. + */ + +object AppInjector { + fun init(app: TuskyApplication) { + DaggerAppComponent.builder().application(app) + .build().inject(app) + + app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + handleActivity(activity) + } + + override fun onActivityPaused(activity: Activity?) { + } + + override fun onActivityResumed(activity: Activity?) { + } + + override fun onActivityStarted(activity: Activity?) { + } + + override fun onActivityDestroyed(activity: Activity?) { + } + + override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { + } + + override fun onActivityStopped(activity: Activity?) { + } + + }) + } + + private fun handleActivity(activity: Activity) { + if (activity is HasAndroidInjector || activity is Injectable) { + AndroidInjection.inject(activity) + } + if (activity is FragmentActivity) { + activity.supportFragmentManager.registerFragmentLifecycleCallbacks( + object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) { + if (f is Injectable) { + AndroidSupportInjection.inject(f) + } + } + }, true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt new file mode 100644 index 0000000..db1317e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -0,0 +1,91 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + + +package com.keylesspalace.tusky.di + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.preference.PreferenceManager +import androidx.room.Room +import com.keylesspalace.tusky.TuskyApplication +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.EventHubImpl +import com.keylesspalace.tusky.components.notifications.Notifier +import com.keylesspalace.tusky.components.notifications.SystemNotifier +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.network.TimelineCasesImpl +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +/** + * Created by charlag on 3/21/18. + */ + +@Module +class AppModule { + + @Provides + fun providesApplication(app: TuskyApplication): Application = app + + @Provides + fun providesContext(app: Application): Context = app + + @Provides + fun providesSharedPreferences(app: Application): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(app) + } + + @Provides + fun providesBroadcastManager(app: Application): LocalBroadcastManager { + return LocalBroadcastManager.getInstance(app) + } + + @Provides + fun providesTimelineUseCases(api: MastodonApi, + eventHub: EventHub): TimelineCases { + return TimelineCasesImpl(api, eventHub) + } + + @Provides + @Singleton + fun providesEventHub(): EventHub = EventHubImpl + + @Provides + @Singleton + fun providesDatabase(appContext: Context): AppDatabase { + return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") + .allowMainThreadQueries() + .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, + AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, + AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, + AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, + AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, + AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, + AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, + AppDatabase.MIGRATION_25_26, AppDatabase.MIGRATION_26_27) + .build() + } + + @Provides + @Singleton + fun notifier(context: Context): Notifier = SystemNotifier(context) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt new file mode 100644 index 0000000..edf9534 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt @@ -0,0 +1,31 @@ +/* Copyright 2018 Jeremiasz Nelz + * Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver +import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class BroadcastReceiverModule { + @ContributesAndroidInjector + abstract fun contributeSendStatusBroadcastReceiver() : SendStatusBroadcastReceiver + + @ContributesAndroidInjector + abstract fun contributeNotificationClearBroadcastReceiver() : NotificationClearBroadcastReceiver +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt new file mode 100644 index 0000000..ca95183 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -0,0 +1,95 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + + +package com.keylesspalace.tusky.di + +import com.keylesspalace.tusky.AccountsInListFragment +import com.keylesspalace.tusky.components.conversation.ConversationsFragment +import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment +import com.keylesspalace.tusky.fragment.* +import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment +import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment +import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment +import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment +import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment +import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment +import com.keylesspalace.tusky.components.preference.PreferencesFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Created by charlag on 3/24/18. + */ + +@Module +abstract class FragmentBuildersModule { + @ContributesAndroidInjector + abstract fun accountListFragment(): AccountListFragment + + @ContributesAndroidInjector + abstract fun accountMediaFragment(): AccountMediaFragment + + @ContributesAndroidInjector + abstract fun viewThreadFragment(): ViewThreadFragment + + @ContributesAndroidInjector + abstract fun timelineFragment(): TimelineFragment + + @ContributesAndroidInjector + abstract fun chatsFragment(): ChatsFragment + + @ContributesAndroidInjector + abstract fun notificationsFragment(): NotificationsFragment + + @ContributesAndroidInjector + abstract fun searchFragment(): SearchStatusesFragment + + @ContributesAndroidInjector + abstract fun notificationPreferencesFragment(): NotificationPreferencesFragment + + @ContributesAndroidInjector + abstract fun accountPreferencesFragment(): AccountPreferencesFragment + + @ContributesAndroidInjector + abstract fun directMessagesPreferencesFragment(): ConversationsFragment + + @ContributesAndroidInjector + abstract fun accountInListsFragment(): AccountsInListFragment + + @ContributesAndroidInjector + abstract fun reportStatusesFragment(): ReportStatusesFragment + + @ContributesAndroidInjector + abstract fun reportNoteFragment(): ReportNoteFragment + + @ContributesAndroidInjector + abstract fun reportDoneFragment(): ReportDoneFragment + + @ContributesAndroidInjector + abstract fun instanceListFragment(): InstanceListFragment + + @ContributesAndroidInjector + abstract fun searchAccountFragment(): SearchAccountsFragment + + @ContributesAndroidInjector + abstract fun searchHashtagsFragment(): SearchHashtagsFragment + + @ContributesAndroidInjector + abstract fun preferencesFragment(): PreferencesFragment + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/GlideModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/GlideModule.kt new file mode 100644 index 0000000..3a1bc0d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/GlideModule.kt @@ -0,0 +1,12 @@ +package com.keylesspalace.tusky.di + +import com.keylesspalace.tusky.util.OmittedDomainAppModule +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class GlideModule { + @ContributesAndroidInjector + abstract fun provideOmittedDomainAppModule() : OmittedDomainAppModule + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt b/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt new file mode 100644 index 0000000..1df715e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt @@ -0,0 +1,23 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + + +package com.keylesspalace.tusky.di + +/** + * Created by charlag on 3/24/18. + */ + +interface Injectable \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt new file mode 100644 index 0000000..641bab5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt @@ -0,0 +1,30 @@ +/* Copyright 2019 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.di + +import android.content.Context +import com.keylesspalace.tusky.components.common.MediaUploader +import com.keylesspalace.tusky.components.common.MediaUploaderImpl +import com.keylesspalace.tusky.network.MastodonApi +import dagger.Module +import dagger.Provides + +@Module +class MediaUploaderModule { + @Provides + fun providesMediaUploder(context: Context, mastodonApi: MastodonApi): MediaUploader = + MediaUploaderImpl(context, mastodonApi) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt new file mode 100644 index 0000000..64611d4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -0,0 +1,89 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import android.content.Context +import android.text.Spanned +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.json.SpannedTypeAdapter +import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.OkHttpUtils +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +/** + * Created by charlag on 3/24/18. + */ + +@Module +class NetworkModule { + + @Provides + @Singleton + fun providesGson(): Gson { + return GsonBuilder() + .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) + .create() + } + + @Provides + @Singleton + fun providesHttpClient( + accountManager: AccountManager, + context: Context + ): OkHttpClient { + return OkHttpUtils.getCompatibleClientBuilder(context) + .apply { + addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + if (BuildConfig.DEBUG) { + addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + //level = HttpLoggingInterceptor.Level.HEADERS + //level = HttpLoggingInterceptor.Level.BODY + }) + } + } + .build() + } + + @Provides + @Singleton + fun providesRetrofit( + httpClient: OkHttpClient, + gson: Gson + ): Retrofit { + return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) + .client(httpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) + .build() + + } + + @Provides + @Singleton + fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create(MastodonApi::class.java) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt new file mode 100644 index 0000000..7152a88 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt @@ -0,0 +1,35 @@ +package com.keylesspalace.tusky.di + +import com.google.gson.Gson +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.ChatRepository +import com.keylesspalace.tusky.repository.ChatRepositoryImpl +import com.keylesspalace.tusky.repository.TimelineRepository +import com.keylesspalace.tusky.repository.TimelineRepositoryImpl +import dagger.Module +import dagger.Provides + +@Module +class RepositoryModule { + @Provides + fun providesTimelineRepository( + db: AppDatabase, + mastodonApi: MastodonApi, + accountManager: AccountManager, + gson: Gson + ): TimelineRepository { + return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson) + } + + @Provides + fun providesChatRepository( + db: AppDatabase, + mastodonApi: MastodonApi, + accountManager: AccountManager, + gson: Gson + ): ChatRepository { + return ChatRepositoryImpl(db.chatsDao(), mastodonApi, accountManager, gson) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt new file mode 100644 index 0000000..a2d6d46 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt @@ -0,0 +1,43 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.di + +import android.content.Context +import com.keylesspalace.tusky.service.SendTootService +import com.keylesspalace.tusky.service.ServiceClient +import com.keylesspalace.tusky.service.ServiceClientImpl +import com.keylesspalace.tusky.service.StreamingService +import dagger.Module +import dagger.Provides +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ServicesModule { + @ContributesAndroidInjector + abstract fun contributesSendTootService(): SendTootService + + @ContributesAndroidInjector + abstract fun contributesStreamingService(): StreamingService + + @Module + companion object { + @Provides + @JvmStatic + fun providesServiceClient(context: Context): ServiceClient { + return ServiceClientImpl(context) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt new file mode 100644 index 0000000..13cccdc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -0,0 +1,107 @@ +// from https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455 + +package com.keylesspalace.tusky.di + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.keylesspalace.tusky.components.chat.ChatViewModel +import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel +import com.keylesspalace.tusky.components.compose.ComposeViewModel +import com.keylesspalace.tusky.components.conversation.ConversationsViewModel +import com.keylesspalace.tusky.components.drafts.DraftsViewModel +import com.keylesspalace.tusky.components.report.ReportViewModel +import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel +import com.keylesspalace.tusky.components.search.SearchViewModel +import com.keylesspalace.tusky.viewmodel.AccountViewModel +import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel +import com.keylesspalace.tusky.viewmodel.EditProfileViewModel +import com.keylesspalace.tusky.viewmodel.ListsViewModel +import dagger.Binds +import dagger.MapKey +import dagger.Module +import dagger.multibindings.IntoMap +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.reflect.KClass + +@Singleton +class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T +} + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@MapKey +internal annotation class ViewModelKey(val value: KClass) + +@Module +abstract class ViewModelModule { + + @Binds + internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory + + @Binds + @IntoMap + @ViewModelKey(AccountViewModel::class) + internal abstract fun accountViewModel(viewModel: AccountViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(EditProfileViewModel::class) + internal abstract fun editProfileViewModel(viewModel: EditProfileViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationsViewModel::class) + internal abstract fun conversationsViewModel(viewModel: ConversationsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ListsViewModel::class) + internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel + + + @Binds + @IntoMap + @ViewModelKey(AccountsInListViewModel::class) + internal abstract fun accountsInListViewModel(viewModel: AccountsInListViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ReportViewModel::class) + internal abstract fun reportViewModel(viewModel: ReportViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(SearchViewModel::class) + internal abstract fun searchViewModel(viewModel: SearchViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ComposeViewModel::class) + internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ScheduledTootViewModel::class) + internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ChatViewModel::class) + internal abstract fun chatViewModel(viewModel: ChatViewModel) : ViewModel + + @Binds + @IntoMap + @ViewModelKey(AnnouncementsViewModel::class) + internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(DraftsViewModel::class) + internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel + + //Add more ViewModels here +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt new file mode 100644 index 0000000..1810788 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt @@ -0,0 +1,22 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class AccessToken( + @SerializedName("access_token") val accessToken: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt new file mode 100644 index 0000000..d7540b8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -0,0 +1,105 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.text.Spanned +import com.google.gson.annotations.SerializedName +import java.util.Date + +data class Account( + val id: String, + @SerializedName("username") val localUsername: String, + @SerializedName("acct") val username: String, + @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract + val note: Spanned, + val url: String, + val avatar: String, + val header: String, + val locked: Boolean = false, + @SerializedName("followers_count") val followersCount: Int = 0, + @SerializedName("following_count") val followingCount: Int = 0, + @SerializedName("statuses_count") val statusesCount: Int = 0, + val source: AccountSource? = null, + val bot: Boolean = false, + val emojis: List? = emptyList(), // nullable for backward compatibility + val fields: List? = emptyList(), //nullable for backward compatibility + val moved: Account? = null, + val pleroma: PleromaAccount? = null +) { + + val name: String + get() = if (displayName.isNullOrEmpty()) { + localUsername + } else displayName + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Account) { + return false + } + return other.id == this.id + } + + fun deepEquals(other: Account): Boolean { + return id == other.id + && localUsername == other.localUsername + && displayName == other.displayName + && note == other.note + && url == other.url + && avatar == other.avatar + && header == other.header + && locked == other.locked + && followersCount == other.followersCount + && followingCount == other.followingCount + && statusesCount == other.statusesCount + && source == other.source + && bot == other.bot + && emojis == other.emojis + && fields == other.fields + && moved == other.moved + && pleroma == other.pleroma + } + + fun isRemote(): Boolean = this.username != this.localUsername +} + +data class AccountSource( + val privacy: Status.Visibility, + val sensitive: Boolean, + val note: String, + val fields: List? +) + +data class Field ( + val name: String, + val value: Spanned, + @SerializedName("verified_at") val verifiedAt: Date? +) + +data class StringField ( + val name: String, + val value: String +) + +data class PleromaAccount( + @SerializedName("ap_id") val apId: String? = null, + @SerializedName("accepts_chat_messages") val acceptsChatMessages: Boolean? = null, + @SerializedName("is_moderator") val isModerator: Boolean? = null, + @SerializedName("is_admin") val isAdmin: Boolean? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt new file mode 100644 index 0000000..5cd32fe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -0,0 +1,57 @@ +/* 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.entity + +import android.text.Spanned +import com.google.gson.annotations.SerializedName +import java.util.* + +data class Announcement( + val id: String, + val content: Spanned, + @SerializedName("starts_at") val startsAt: Date?, + @SerializedName("ends_at") val endsAt: Date?, + @SerializedName("all_day") val allDay: Boolean, + @SerializedName("published_at") val publishedAt: Date, + @SerializedName("updated_at") val updatedAt: Date, + val read: Boolean, + val mentions: List, + val statuses: List, + val tags: List, + val emojis: List, + val reactions: List +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + val announcement = other as Announcement? + return id == announcement?.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + data class Reaction( + val name: String, + var count: Int, + var me: Boolean, + val url: String?, + @SerializedName("static_url") val staticUrl: String? + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt new file mode 100644 index 0000000..95a829c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt @@ -0,0 +1,23 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class AppCredentials( + @SerializedName("client_id") val clientId: String, + @SerializedName("client_secret") val clientSecret: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt new file mode 100644 index 0000000..587c763 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt @@ -0,0 +1,95 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.annotations.JsonAdapter +import com.google.gson.annotations.SerializedName +import com.keylesspalace.tusky.R +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Attachment( + val id: String, + val url: String, + @SerializedName("preview_url") val previewUrl: String?, // can be null for e.g. audio attachments + val meta: MetaData?, + val type: Type, + val description: String?, + val blurhash: String? +) : Parcelable { + + @JsonAdapter(MediaTypeDeserializer::class) + enum class Type { + @SerializedName("image") + IMAGE, + @SerializedName("gifv") + GIFV, + @SerializedName("video") + VIDEO, + @SerializedName("audio") + AUDIO, + @SerializedName("unknown") + UNKNOWN + } + + class MediaTypeDeserializer : JsonDeserializer { + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, classOfT: java.lang.reflect.Type, context: JsonDeserializationContext): Type { + return when (json.toString()) { + "\"image\"" -> Type.IMAGE + "\"gifv\"" -> Type.GIFV + "\"video\"" -> Type.VIDEO + "\"audio\"" -> Type.AUDIO + else -> Type.UNKNOWN + } + } + } + + fun describeAttachmentType() : Int { + return when(type) { + Type.IMAGE -> R.string.attachment_type_image + Type.VIDEO, Type.GIFV -> R.string.attachment_type_video + Type.AUDIO -> R.string.attachment_type_audio + Type.UNKNOWN -> R.string.attachment_type_unknown + } + } + + /** + * The meta data of an [Attachment]. + */ + @Parcelize + data class MetaData ( + val focus: Focus?, + val duration: Float? + ) : Parcelable + + /** + * The Focus entity, used to specify the focal point of an image. + * + * See here for more details what the x and y mean: + * https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point + */ + @Parcelize + data class Focus ( + val x: Float, + val y: Float + ) : Parcelable +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt new file mode 100644 index 0000000..1b07cea --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -0,0 +1,45 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.text.Spanned +import com.google.gson.annotations.SerializedName + +data class Card( + val url: String, + val title: Spanned, + val description: Spanned, + @SerializedName("author_name") val authorName: String, + val image: String, + val type: String, + val width: Int, + val height: Int, + val blurhash: String? +) { + + override fun hashCode(): Int { + return url.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Card) { + return false + } + val account = other as Card? + return account?.url == this.url + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Chat.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Chat.kt new file mode 100644 index 0000000..35c6ce1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Chat.kt @@ -0,0 +1,44 @@ +/* Copyright 2020 Alibek Omarov + * + * This file is a part of Husky. + * + * 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. + * + * Husky 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 Husky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.text.Spanned +import com.google.gson.annotations.SerializedName +import java.util.* + +data class ChatMessage( + val id: String, + val content: Spanned?, + @SerializedName("chat_id") val chatId: String, + @SerializedName("account_id") val accountId: String, + @SerializedName("created_at") val createdAt: Date, + val attachment: Attachment?, + val emojis: List, + val card: Card? +) + +data class Chat( + val account: Account, + val id: String, + val unread: Long, + @SerializedName("last_message") val lastMessage: ChatMessage?, + @SerializedName("updated_at") val updatedAt: Date +) + +data class NewChatMessage( + val content: String, + val media_id: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt new file mode 100644 index 0000000..0e66385 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt @@ -0,0 +1,25 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class Conversation( + val id: String, + val accounts: List, + @SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038 + val unread: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt new file mode 100644 index 0000000..289a93f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -0,0 +1,34 @@ +/* Copyright 2019 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.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +data class DeletedStatus( + var text: String?, + @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("spoiler_text") val spoilerText: String, + val visibility: Status.Visibility, + val sensitive: Boolean, + @SerializedName("media_attachments") var attachments: ArrayList?, + val poll: Poll?, + @SerializedName("created_at") val createdAt: Date +) { + fun isEmpty(): Boolean { + return text == null && attachments == null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt new file mode 100644 index 0000000..029b392 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -0,0 +1,36 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcel +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Emoji( + val shortcode: String, + val url: String, + @SerializedName("static_url") val staticUrl: String, + @SerializedName("visible_in_picker") val visibleInPicker: Boolean? +) : Parcelable + +data class EmojiReaction( + val name: String, + val count: Int, + val me: Boolean, + val accounts: List? // only for emoji_reactions_by +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt new file mode 100644 index 0000000..58bdc79 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -0,0 +1,48 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class Filter ( + val id: String, + val phrase: String, + val context: List, + @SerializedName("expires_at") val expiresAt: String?, + val irreversible: Boolean, + @SerializedName("whole_word") val wholeWord: Boolean +) { + companion object { + const val HOME = "home" + const val NOTIFICATIONS = "notifications" + const val PUBLIC = "public" + const val THREAD = "thread" + const val ACCOUNT = "account" + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Filter) { + return false + } + val filter = other as Filter? + return filter?.id.equals(id) + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt new file mode 100644 index 0000000..1eaaf68 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt @@ -0,0 +1,3 @@ +package com.keylesspalace.tusky.entity + +data class HashTag(val name: String) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt new file mode 100644 index 0000000..9473f03 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt @@ -0,0 +1,9 @@ +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class IdentityProof( + val provider: String, + @SerializedName("provider_username") val username: String, + @SerializedName("profile_url") val profileUrl: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt new file mode 100644 index 0000000..f06d3ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -0,0 +1,70 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class Instance ( + val uri: String, + val title: String, + val description: String, + val email: String, + val version: String, + val urls: Map, + val stats: Map?, + val thumbnail: String?, + val languages: List, + @SerializedName("contact_account") val contactAccount: Account, + @SerializedName("max_toot_chars") val maxTootChars: Int?, + @SerializedName("max_bio_chars") val maxBioChars: Int?, + @SerializedName("poll_limits") val pollLimits: PollLimits?, + @SerializedName("chat_limit") val chatLimit: Int?, + @SerializedName("avatar_upload_limit") val avatarUploadLimit: Long?, + @SerializedName("banner_upload_limit") val bannerUploadLimit: Long?, + @SerializedName("description_limit") val descriptionLimit: Int?, + @SerializedName("upload_limit") val uploadLimit: Long?, + val pleroma: InstancePleroma? +) { + override fun hashCode(): Int { + return uri.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Instance) { + return false + } + val instance = other as Instance? + return instance?.uri.equals(uri) + } +} + +data class InstancePleroma ( + val metadata: InstancePleromaMetadata +) + +data class InstancePleromaMetadata ( + val features: List, + @SerializedName("fields_limits") val fieldsLimits: InstancePleromaMetadataFieldsLimits, +) + +data class InstancePleromaMetadataFieldsLimits( + @SerializedName("max_fields") val maxFields: Int, +) + +data class PollLimits ( + @SerializedName("max_options") val maxOptions: Int?, + @SerializedName("max_option_chars") val maxOptionChars: Int? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt new file mode 100644 index 0000000..16fd9e3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt @@ -0,0 +1,15 @@ +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +/** + * API type for saving the scroll position of a timeline. + */ +data class Marker( + @SerializedName("last_read_id") + val lastReadId: String, + val version: Int, + @SerializedName("updated_at") + val updatedAt: Date +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt new file mode 100644 index 0000000..2f8eecf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt @@ -0,0 +1,26 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky.entity + +/** + * Created by charlag on 1/4/18. + */ + +data class MastoList( + val id: String, + val title: String +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt new file mode 100644 index 0000000..5f64db1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -0,0 +1,40 @@ +/* Copyright 2019 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.entity + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +data class NewStatus( + val status: String, + @SerializedName("spoiler_text") val warningText: String, + @SerializedName("in_reply_to_id") val inReplyToId: String?, + val visibility: String, + val sensitive: Boolean, + @SerializedName("media_ids") val mediaIds: List?, + @SerializedName("scheduled_at") val scheduledAt: String?, + val poll: NewPoll?, + var content_type: String?, + val preview: Boolean? +) + +@Parcelize +data class NewPoll( + val options: List, + @SerializedName("expires_in") val expiresIn: Int, + val multiple: Boolean +): Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NodeInfo.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NodeInfo.kt new file mode 100644 index 0000000..05f858a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NodeInfo.kt @@ -0,0 +1,63 @@ +/* Copyright 2020 Alibek Omarov + * + * This file is a part of Husky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +// .well-known/nodeinfo +data class NodeInfoLink( + val href: String, + val rel: String +) + +data class NodeInfoLinks( + val links: List +) + +// we care only about supported postFormats +// so implement only metadata fetching +data class NodeInfo( + val metadata: NodeInfoMetadata? = null, + val software: NodeInfoSoftware +) + +data class NodeInfoSoftware( + val name: String, + val version: String +) + +data class NodeInfoPleromaUploadLimits( + val avatar: Long?, + val background: Long?, + val banner: Long?, + val general: Long? +) + +data class NodeInfoPixelfedUploadLimits( + @SerializedName("max_photo_size") val maxPhotoSize: Long? +) + +data class NodeInfoPixelfedConfig( + val uploader: NodeInfoPixelfedUploadLimits? +) + +data class NodeInfoMetadata( + val postFormats: List?, + val uploadLimits: NodeInfoPleromaUploadLimits?, + val config: NodeInfoPixelfedConfig? +) + diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt new file mode 100644 index 0000000..c57addc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -0,0 +1,107 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.* +import com.google.gson.annotations.SerializedName +import com.google.gson.annotations.JsonAdapter +import java.util.* + +data class PleromaNotification( + @SerializedName("is_seen") val seen: Boolean +) + +data class Notification( + val type: Type, + val id: String, + val account: Account, + val status: Status?, + val pleroma: PleromaNotification? = null, + val emoji: String? = null, + @SerializedName("chat_message") val chatMessage: ChatMessage? = null, + @SerializedName("created_at") val createdAt: Date? = null, + val target: Account? = null) { + + @JsonAdapter(NotificationTypeAdapter::class) + enum class Type(val presentation: String) { + UNKNOWN("unknown"), + MENTION("mention"), + REBLOG("reblog"), + FAVOURITE("favourite"), + FOLLOW("follow"), + POLL("poll"), + EMOJI_REACTION("pleroma:emoji_reaction"), + FOLLOW_REQUEST("follow_request"), + CHAT_MESSAGE("pleroma:chat_mention"), + MOVE("move"), + STATUS("status"); /* Mastodon 3.3.0rc1 */ + + companion object { + + @JvmStatic + fun byString(s: String): Type { + values().forEach { + if (s == it.presentation) + return it + } + return UNKNOWN + } + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE, MOVE, STATUS) + + val asStringList = asList.map { it.presentation } + } + + override fun toString(): String { + return presentation + } + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Notification) { + return false + } + val notification = other as Notification? + return notification?.id == this.id + } + + class NotificationTypeAdapter : JsonDeserializer { + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: java.lang.reflect.Type, context: JsonDeserializationContext): Type { + return Type.byString(json.asString) + } + + } + + companion object { + + // for Pleroma compatibility that uses Mention type + @JvmStatic + fun rewriteToStatusTypeIfNeeded(body: Notification, accountId: String) : Notification { + if (body.type == Type.MENTION + && body.status != null) { + return if (body.status.mentions.any { + it.id == accountId + }) body else body.copy(type = Type.STATUS) + } + return body + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt new file mode 100644 index 0000000..0d47c6f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt @@ -0,0 +1,47 @@ +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +data class Poll( + val id: String, + @SerializedName("expires_at") val expiresAt: Date?, + val expired: Boolean, + val multiple: Boolean, + @SerializedName("votes_count") val votesCount: Int, + @SerializedName("voters_count") val votersCount: Int?, // nullable for compatibility with Pleroma + val options: List, + val voted: Boolean +) { + + fun votedCopy(choices: List): Poll { + val newOptions = options.mapIndexed { index, option -> + if(choices.contains(index)) { + option.copy(votesCount = option.votesCount + 1) + } else { + option + } + } + + return copy( + options = newOptions, + votesCount = votesCount + choices.size, + votersCount = votersCount?.plus(1), + voted = true + ) + } + + fun toNewPoll(creationDate: Date) = NewPoll( + options.map { it.title }, + expiresAt?.let { + ((it.time - creationDate.time) / 1000).toInt() + 1 + }?: 3600, + multiple + ) + +} + +data class PollOption( + val title: String, + @SerializedName("votes_count") val votesCount: Int +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt new file mode 100644 index 0000000..e25a3d1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -0,0 +1,33 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class Relationship ( + val id: String, + val following: Boolean, + @SerializedName("followed_by") val followedBy: Boolean, + val blocking: Boolean, + val muting: Boolean, + @SerializedName("muting_notifications") val mutingNotifications: Boolean, + val requested: Boolean, + @SerializedName("showing_reblogs") val showingReblogs: Boolean, + val subscribing: Boolean? = null, // Pleroma extension + @SerializedName("domain_blocking") val blockingDomain: Boolean, + val note: String?, // nullable for backward compatibility / feature detection + val notifying: Boolean? // since 3.3.0rc +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt new file mode 100644 index 0000000..2621bd5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt @@ -0,0 +1,25 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class ScheduledStatus( + val id: String, + @SerializedName("scheduled_at") val scheduledAt: String, + val params: StatusParams, + @SerializedName("media_attachments") val mediaAttachments: ArrayList +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt new file mode 100644 index 0000000..4307380 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt @@ -0,0 +1,22 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +data class SearchResult ( + val accounts: List, + val statuses: List, + val hashtags: List +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt new file mode 100644 index 0000000..47dc44d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -0,0 +1,214 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.URLSpan +import com.google.gson.annotations.SerializedName +import java.util.* + +data class Status( + var id: String, + var url: String?, // not present if it's reblog + val account: Account, + @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, + val reblog: Status?, + val content: Spanned, + @SerializedName("created_at") val createdAt: Date, + val emojis: List, + @SerializedName("reblogs_count") val reblogsCount: Int, + @SerializedName("favourites_count") val favouritesCount: Int, + var reblogged: Boolean, + var favourited: Boolean, + var bookmarked: Boolean, + var sensitive: Boolean, + @SerializedName("spoiler_text") val spoilerText: String, + val visibility: Visibility, + @SerializedName("media_attachments") var attachments: ArrayList, + val mentions: Array, + val application: Application?, + var pinned: Boolean?, + val poll: Poll?, + val card: Card?, + var content_type: String? = null, + val pleroma: PleromaStatus? = null, + var muted: Boolean = false /* set when either thread or user is muted */ +) { + + val actionableId: String + get() = reblog?.id ?: id + + val actionableStatus: Status + get() = reblog ?: this + + enum class Visibility(val num: Int) { + UNKNOWN(0), + @SerializedName("public") + PUBLIC(1), + @SerializedName("unlisted") + UNLISTED(2), + @SerializedName("private") + PRIVATE(3), + @SerializedName("direct") + DIRECT(4); + + fun serverString(): String { + return when (this) { + PUBLIC -> "public" + UNLISTED -> "unlisted" + PRIVATE -> "private" + DIRECT -> "direct" + UNKNOWN -> "unknown" + } + } + + companion object { + + @JvmStatic + fun byNum(num: Int): Visibility { + return when (num) { + 4 -> DIRECT + 3 -> PRIVATE + 2 -> UNLISTED + 1 -> PUBLIC + 0 -> UNKNOWN + else -> UNKNOWN + } + } + + @JvmStatic + fun byString(s: String): Visibility { + return when (s) { + "public" -> PUBLIC + "unlisted" -> UNLISTED + "private" -> PRIVATE + "direct" -> DIRECT + "unknown" -> UNKNOWN + else -> UNKNOWN + } + } + } + } + + fun rebloggingAllowed(): Boolean { + return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN) + } + + fun isPinned(): Boolean { + return pinned ?: false + } + + fun toDeletedStatus(): DeletedStatus { + return DeletedStatus( + text = getEditableText(), + inReplyToId = inReplyToId, + spoilerText = spoilerText, + visibility = visibility, + sensitive = sensitive, + attachments = attachments, + poll = poll, + createdAt = createdAt + ) + } + + fun isMuted(): Boolean { + return muted + } + + fun isUserMuted(): Boolean { + return muted && !isThreadMuted() + } + + fun isThreadMuted(): Boolean { + return pleroma?.threadMuted ?: false + } + + fun setThreadMuted(mute: Boolean) { + if(pleroma?.threadMuted != null) + pleroma.threadMuted = mute + } + + fun getConversationId(): Int { + return pleroma?.conversationId ?: -1 + } + + fun getEmojiReactions(): List? { + return pleroma?.emojiReactions; + } + + fun getInReplyToAccountAcct(): String? { + return pleroma?.inReplyToAccountAcct; + } + + fun getParentVisible(): Boolean { + return pleroma?.parentVisible ?: true; + } + + private fun getEditableText(): String { + val builder = SpannableStringBuilder(content) + for (span in content.getSpans(0, content.length, URLSpan::class.java)) { + val url = span.url + for ((_, url1, username) in mentions) { + if (url == url1) { + val start = builder.getSpanStart(span) + val end = builder.getSpanEnd(span) + builder.replace(start, end, "@$username") + break + } + } + } + return builder.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + val status = other as Status? + return id == status?.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + data class PleromaStatus( + @SerializedName("thread_muted") var threadMuted: Boolean?, + @SerializedName("conversation_id") val conversationId: Int?, + @SerializedName("emoji_reactions") val emojiReactions: List?, + @SerializedName("in_reply_to_account_acct") val inReplyToAccountAcct: String?, + @SerializedName("parent_visible") val parentVisible: Boolean? + ) + + data class Mention ( + val id: String, + val url: String?, // can be null due to bug in some Pleroma versions + @SerializedName("acct") val username: String, + @SerializedName("username") val localUsername: String + ) + + data class Application ( + val name: String, + val website: String? + ) + + companion object { + const val MAX_MEDIA_ATTACHMENTS = 4 + const val MAX_POLL_OPTIONS = 4 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt new file mode 100644 index 0000000..1287619 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt @@ -0,0 +1,21 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +data class StatusContext ( + val ancestors: List, + val descendants: List +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt new file mode 100644 index 0000000..0e25e6c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt @@ -0,0 +1,26 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class StatusParams( + val text: String, + val sensitive: Boolean, + val visibility: Status.Visibility, + @SerializedName("spoiler_text") val spoilerText: String, + @SerializedName("in_reply_to_id") val inReplyToId: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Sticker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Sticker.kt new file mode 100644 index 0000000..0f7746e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Sticker.kt @@ -0,0 +1,30 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +data class StickerPack( + val title: String, + val tabIcon: String, + val stickers: List, + var internal_url: String = "" +) : Comparable { + override fun compareTo(pack: StickerPack) : Int { + return title.compareTo(pack.title) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StreamEvent.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StreamEvent.kt new file mode 100644 index 0000000..ce761ed --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StreamEvent.kt @@ -0,0 +1,20 @@ +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName + +data class StreamEvent ( + val event: EventType, + val payload: String +) { + enum class EventType { + UNKNOWN, + @SerializedName("update") + UPDATE, + @SerializedName("notification") + NOTIFICATION, + @SerializedName("delete") + DELETE, + @SerializedName("filters_changed") + FILTERS_CHANGED; + } +} \ 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 new file mode 100644 index 0000000..340a998 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -0,0 +1,417 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.fragment + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.AccountActivity +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.di.Injectable +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.interfaces.AccountActionListener +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.view.EndlessOnScrollListener +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDispose +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_account_list.* +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import java.util.* +import javax.inject.Inject + +class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { + + @Inject + lateinit var api: MastodonApi + + private lateinit var type: Type + private var id: String? = null + private var emojiReaction: String? = null + + private lateinit var scrollListener: EndlessOnScrollListener + private lateinit var adapter: AccountAdapter + private var fetching = false + private var bottomId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + type = arguments?.getSerializable(ARG_TYPE) as Type + id = arguments?.getString(ARG_ID) + emojiReaction = arguments?.getString(ARG_EMOJI) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment_account_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.setHasFixedSize(true) + val layoutManager = LinearLayoutManager(view.context) + recyclerView.layoutManager = layoutManager + (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + + adapter = when (type) { + Type.BLOCKS -> BlocksAdapter(this) + Type.MUTES -> MutesAdapter(this) + Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) + else -> FollowAdapter(this) + } + recyclerView.adapter = adapter + + scrollListener = object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + if (bottomId == null) { + return + } + fetchAccounts(bottomId) + } + } + + recyclerView.addOnScrollListener(scrollListener) + + fetchAccounts() + } + + override fun onViewAccount(id: String) { + (activity as BaseActivity?)?.let { + val intent = AccountActivity.getIntent(it, id) + it.startActivityWithSlideInAnimation(intent) + } + } + + override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { + if (!mute) { + api.unmuteAccount(id) + } else { + api.muteAccount(id, notifications) + } + .autoDispose(from(this)) + .subscribe({ + onMuteSuccess(mute, id, position, notifications) + }, { + onMuteFailure(mute, id, notifications) + }) + } + + private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { + val mutesAdapter = adapter as MutesAdapter + if (muted) { + mutesAdapter.updateMutingNotifications(id, notifications, position) + return + } + val unmutedUser = mutesAdapter.removeItem(position) + + if (unmutedUser != null) { + Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + mutesAdapter.addItem(unmutedUser, position) + onMute(true, id, position, notifications) + } + .show() + } + } + + private fun onMuteFailure(mute: Boolean, accountId: String, notifications: Boolean) { + val verb = if (mute) { + if (notifications) { + "mute (notifications = true)" + } else { + "mute (notifications = false)" + } + } else { + "unmute" + } + Log.e(TAG, "Failed to $verb account id $accountId") + } + + override fun onBlock(block: Boolean, id: String, position: Int) { + if (!block) { + api.unblockAccount(id) + } else { + api.blockAccount(id) + } + .autoDispose(from(this)) + .subscribe({ + onBlockSuccess(block, id, position) + }, { + onBlockFailure(block, id) + }) + } + + private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { + if (blocked) { + return + } + val blocksAdapter = adapter as BlocksAdapter + val unblockedUser = blocksAdapter.removeItem(position) + + if (unblockedUser != null) { + Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + blocksAdapter.addItem(unblockedUser, position) + onBlock(true, id, position) + } + .show() + } + } + + private fun onBlockFailure(block: Boolean, accountId: String) { + val verb = if (block) { + "block" + } else { + "unblock" + } + Log.e(TAG, "Failed to $verb account accountId $accountId") + } + + override fun onRespondToFollowRequest(accept: Boolean, accountId: String, + position: Int) { + + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onRespondToFollowRequestSuccess(position) + } else { + onRespondToFollowRequestFailure(accept, accountId) + } + } + + override fun onFailure(call: Call, t: Throwable) { + onRespondToFollowRequestFailure(accept, accountId) + } + } + + val call = if (accept) { + api.authorizeFollowRequest(accountId) + } else { + api.rejectFollowRequest(accountId) + } + callList.add(call) + call.enqueue(callback) + } + + private fun onRespondToFollowRequestSuccess(position: Int) { + val followRequestsAdapter = adapter as FollowRequestsAdapter + followRequestsAdapter.removeItem(position) + } + + private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $accountId.") + } + + private fun getFetchCallByListType(fromId: String?): Single>> { + return when (type) { + Type.FOLLOWS -> { + val accountId = requireId(type, id) + api.accountFollowing(accountId, fromId) + } + Type.FOLLOWERS -> { + val accountId = requireId(type, id) + api.accountFollowers(accountId, fromId) + } + Type.BLOCKS -> api.blocks(fromId) + Type.MUTES -> api.mutes(fromId) + Type.FOLLOW_REQUESTS -> api.followRequests(fromId) + Type.REBLOGGED -> { + val statusId = requireId(type, id) + api.statusRebloggedBy(statusId, fromId) + } + Type.FAVOURITED -> { + val statusId = requireId(type, id) + api.statusFavouritedBy(statusId, fromId) + } + Type.REACTED -> { + // HACKHACK: make compiler happy + val statusId = requireId(type, id) + api.statusFavouritedBy(statusId, fromId) + } + } + } + + private fun requireId(type: Type, id: String?, name: String = "id"): String { + return requireNotNull(id) { name+" must not be null for type "+type.name } + } + + private fun getEmojiReactionFetchCall(): Single>> { + val statusId = requireId(type, id) + val emoji = requireId(type, emojiReaction, "emoji") + return api.statusReactedBy(statusId, emoji) + } + + private fun fetchAccounts(fromId: String? = null) { + if (fetching) { + return + } + fetching = true + + if (fromId != null) { + recyclerView.post { adapter.setBottomLoading(true) } + } + + if(type == Type.REACTED) { + getEmojiReactionFetchCall() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ response -> + val emojiReaction = response.body() + + if (response.isSuccessful && emojiReaction != null && emojiReaction.size > 0 && emojiReaction.get(0).accounts != null) { + val linkHeader = response.headers()["Link"] + onFetchAccountsSuccess(emojiReaction.get(0).accounts!!, linkHeader) + } else { + onFetchAccountsFailure(Exception(response.message())) + } + }, {throwable -> + onFetchAccountsFailure(throwable) + }) + } else { + getFetchCallByListType(fromId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ response -> + val accountList = response.body() + + if (response.isSuccessful && accountList != null) { + val linkHeader = response.headers()["Link"] + onFetchAccountsSuccess(accountList, linkHeader) + } else { + onFetchAccountsFailure(Exception(response.message())) + } + }, {throwable -> + onFetchAccountsFailure(throwable) + }) + } + } + + private fun onFetchAccountsSuccess(accounts: List, linkHeader: String?) { + adapter.setBottomLoading(false) + + val links = HttpHeaderLink.parse(linkHeader) + val next = HttpHeaderLink.findByRelationType(links, "next") + val fromId = next?.uri?.getQueryParameter("max_id") + + if (adapter.itemCount > 0) { + adapter.addItems(accounts) + } else { + adapter.update(accounts) + } + + if (adapter is MutesAdapter) { + fetchRelationships(accounts.map { it.id }) + } + + bottomId = fromId + + fetching = false + + if (adapter.itemCount == 0) { + messageView.show() + messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } else { + messageView.hide() + } + } + + private fun fetchRelationships(ids: List) { + api.relationships(ids) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe(::onFetchRelationshipsSuccess) { + onFetchRelationshipsFailure(ids) + } + } + + private fun onFetchRelationshipsSuccess(relationships: List) { + val mutesAdapter = adapter as MutesAdapter + var mutingNotificationsMap = HashMap() + relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) } + mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap) + } + + private fun onFetchRelationshipsFailure(ids: List) { + Log.e(TAG, "Fetch failure for relationships of accounts: $ids") + } + + private fun onFetchAccountsFailure(throwable: Throwable) { + fetching = false + Log.e(TAG, "Fetch failure", throwable) + + if (adapter.itemCount == 0) { + messageView.show() + if (throwable is IOException) { + messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + messageView.hide() + this.fetchAccounts(null) + } + } else { + messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + messageView.hide() + this.fetchAccounts(null) + } + } + } + } + + companion object { + private const val TAG = "AccountList" // logging tag + private const val ARG_TYPE = "type" + private const val ARG_ID = "id" + private const val ARG_EMOJI = "emoji" + + fun newInstance(type: Type, id: String? = null, emoji: String? = null): AccountListFragment { + return AccountListFragment().apply { + arguments = Bundle(3).apply { + putSerializable(ARG_TYPE, type) + putString(ARG_ID, id) + putString(ARG_EMOJI, emoji) + } + } + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt new file mode 100644 index 0000000..5e9ed0e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -0,0 +1,356 @@ +/* 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.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.view.SquareImageView +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlinx.android.synthetic.main.fragment_timeline.* +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import java.util.* +import javax.inject.Inject + +/** + * Created by charlag on 26/10/2017. + * + * Fragment with multiple columns of media previews for the specified account. + */ + +class AccountMediaFragment : BaseFragment(), 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 + private var filterMuted = false + + @Inject + lateinit var api: MastodonApi + + private val adapter = MediaGridAdapter() + private var currentCall: Call>? = null + private val statuses = mutableListOf() + private var fetchingStatus = FetchingStatus.NOT_FETCHING + + private lateinit var accountId: String + + private val callback = object : Callback> { + override fun onFailure(call: Call>?, t: Throwable?) { + fetchingStatus = FetchingStatus.NOT_FETCHING + + if (isAdded) { + swipeRefreshLayout.isRefreshing = false + progressBar.visibility = View.GONE + topProgressBar?.hide() + statusView.show() + if (t is IOException) { + statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + doInitialLoadingIfNeeded() + } + } else { + statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + doInitialLoadingIfNeeded() + } + } + } + + Log.d(TAG, "Failed to fetch account media", t) + } + + override fun onResponse(call: Call>, response: Response>) { + fetchingStatus = FetchingStatus.NOT_FETCHING + if (isAdded) { + swipeRefreshLayout.isRefreshing = false + progressBar.visibility = View.GONE + topProgressBar?.hide() + + val body = response.body() + body?.let { fetched -> + // filter muted statuses if needed + val filtered = fetched.filter { !(filterMuted && it.muted) } + statuses.addAll(0, filtered) + // flatMap requires iterable but I don't want to box each array into list + val result = mutableListOf() + for (status in filtered) { + result.addAll(AttachmentViewData.list(status)) + } + adapter.addTop(result) + if (result.isNotEmpty()) + recyclerView.scrollToPosition(0) + + if (statuses.isEmpty()) { + statusView.show() + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, + null) + } + } + } + } + } + + private val bottomCallback = object : Callback> { + override fun onFailure(call: Call>?, t: Throwable?) { + fetchingStatus = FetchingStatus.NOT_FETCHING + + Log.d(TAG, "Failed to fetch account media", t) + } + + override fun onResponse(call: Call>, response: Response>) { + fetchingStatus = FetchingStatus.NOT_FETCHING + val body = response.body() + body?.let { fetched -> + Log.d(TAG, "fetched ${fetched.size} statuses") + if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}") + + // filter muted statuses if needed + val filtered = fetched.filter { !(filterMuted && it.muted) } + + statuses.addAll(filtered) + Log.d(TAG, "now there are ${statuses.size} statuses") + // flatMap requires iterable but I don't want to box each array into list + val result = mutableListOf() + for (status in filtered) { + result.addAll(AttachmentViewData.list(status)) + } + adapter.addBottom(result) + } + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true + accountId = arguments?.getString(ACCOUNT_ID_ARG)!! + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_timeline, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + + val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count) + val layoutManager = GridLayoutManager(view.context, columnCount) + + adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) + + recyclerView.layoutManager = layoutManager + recyclerView.adapter = adapter + + if (isSwipeToRefreshEnabled) { + swipeRefreshLayout.setOnRefreshListener { + refresh() + } + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + statusView.visibility = View.GONE + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) { + if (dy > 0) { + val itemCount = layoutManager.itemCount + val lastItem = layoutManager.findLastCompletelyVisibleItemPosition() + if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) { + statuses.lastOrNull()?.let { last -> + Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)") + fetchingStatus = FetchingStatus.FETCHING_BOTTOM + currentCall = api.accountStatuses(accountId, last.id, null, null, null, true, null) + currentCall?.enqueue(bottomCallback) + } + } + } + } + }) + + filterMuted = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean( + PrefKeys.HIDE_MUTED_USERS, false + ) + + doInitialLoadingIfNeeded() + } + + private fun refresh() { + statusView.hide() + if (fetchingStatus != FetchingStatus.NOT_FETCHING) return + currentCall = if (statuses.isEmpty()) { + fetchingStatus = FetchingStatus.INITIAL_FETCHING + api.accountStatuses(accountId, null, null, null, null, true, null) + } else { + fetchingStatus = FetchingStatus.REFRESHING + api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) + } + currentCall?.enqueue(callback) + + if (!isSwipeToRefreshEnabled) + topProgressBar?.show() + } + + private fun doInitialLoadingIfNeeded() { + if (isAdded) { + statusView.hide() + } + if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { + fetchingStatus = FetchingStatus.INITIAL_FETCHING + currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) + currentCall?.enqueue(callback) + } + else if (needToRefresh) + refresh() + needToRefresh = false + } + + private fun viewMedia(items: List, currentIndex: Int, view: View?) { + + when (items[currentIndex].attachment.type) { + Attachment.Type.IMAGE, + Attachment.Type.GIFV, + Attachment.Type.VIDEO, + Attachment.Type.AUDIO -> { + val intent = ViewMediaActivity.newIntent(context, items, currentIndex) + if (view != null && activity != null) { + val url = items[currentIndex].attachment.url + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, url) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Attachment.Type.UNKNOWN -> { + LinkHelper.openLink(items[currentIndex].attachment.url, context) + } + } + } + + private enum class FetchingStatus { + NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING + } + + inner class MediaGridAdapter : + RecyclerView.Adapter() { + + var baseItemColor = Color.BLACK + + private val items = mutableListOf() + private val itemBgBaseHSV = FloatArray(3) + private val random = Random() + + fun addTop(newItems: List) { + items.addAll(0, newItems) + notifyItemRangeInserted(0, newItems.size) + } + + fun addBottom(newItems: List) { + if (newItems.isEmpty()) return + + val oldLen = items.size + items.addAll(newItems) + notifyItemRangeInserted(oldLen, newItems.size) + } + + override fun onAttachedToRecyclerView(recycler_view: RecyclerView) { + val hsv = FloatArray(3) + Color.colorToHSV(baseItemColor, hsv) + super.onAttachedToRecyclerView(recycler_view) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { + val view = SquareImageView(parent.context) + view.scaleType = ImageView.ScaleType.CENTER_CROP + return MediaViewHolder(view) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { + itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f + holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) + val item = items[position] + + Glide.with(holder.imageView) + .load(item.attachment.previewUrl) + .centerInside() + .into(holder.imageView) + } + + + inner class MediaViewHolder(val imageView: ImageView) + : RecyclerView.ViewHolder(imageView), + View.OnClickListener { + init { + itemView.setOnClickListener(this) + } + + // saving some allocations + override fun onClick(v: View?) { + viewMedia(items, adapterPosition, imageView) + } + } + } + + override fun refreshContent() { + if (isAdded) + refresh() + else + needToRefresh = true + } + + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java new file mode 100644 index 0000000..b674b8b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java @@ -0,0 +1,43 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.fragment; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; + +public class BaseFragment extends Fragment { + protected List callList; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + callList = new ArrayList<>(); + } + + @Override + public void onDestroy() { + for (Call call : callList) { + call.cancel(); + } + super.onDestroy(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt new file mode 100644 index 0000000..a51cbad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt @@ -0,0 +1,781 @@ +package com.keylesspalace.tusky.fragment + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.arch.core.util.Function +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.* +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.sparkbutton.helpers.Utils +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.PostLookupFallbackBehavior +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.ChatsAdapter +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.TimelineAdapter +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.chat.ChatActivity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Chat +import com.keylesspalace.tusky.entity.ChatMessage +import com.keylesspalace.tusky.entity.NewChatMessage +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.repository.* +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Either.Left +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import com.keylesspalace.tusky.viewdata.ChatViewData +import com.uber.autodispose.AutoDispose +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_timeline.* +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, ReselectableFragment, ChatActionListener, OnRefreshListener { + private val TAG = "ChatsF" // logging tag + private val LOAD_AT_ONCE = 30 + private val BROKEN_PAGINATION_IN_BACKEND = true // break pagination until it's not fixed in plemora + + + @Inject + lateinit var eventHub: EventHub + @Inject + lateinit var api: MastodonApi + @Inject + lateinit var accountManager: AccountManager + @Inject + lateinit var chatRepo: ChatRepository + @Inject + lateinit var timelineCases: TimelineCases + + lateinit var adapter: ChatsAdapter + + lateinit var layoutManager: LinearLayoutManager + + private lateinit var scrollListener: EndlessOnScrollListener + + private lateinit var bottomSheetActivity: BottomSheetActivity + private var hideFab = false + private var bottomLoading = false + + private var eventRegistered = false + private var isSwipeToRefreshEnabled = true + private var isNeedRefresh = false + private var didLoadEverythingBottom = false + private var initialUpdateFailed = false + + private enum class FetchEnd { + TOP, BOTTOM, MIDDLE + } + + private val chats = PairedList(Function {input -> + input.asRightOrNull()?.let(ViewDataUtils::chatToViewData) ?: + ChatViewData.Placeholder(input.asLeft().id, false) + }) + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (isAdded) { + Log.d(TAG, "onInserted"); + adapter.notifyItemRangeInserted(position, count) + // 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) + recyclerView.scrollBy(0, Utils.dpToPx(context!!, -30)); + else + recyclerView.scrollToPosition(0); + } + } + } + + override fun onRemoved(position: Int, count: Int) { + Log.d(TAG, "onRemoved"); + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + Log.d(TAG, "onMoved"); + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + Log.d(TAG, "onChanged"); + adapter.notifyItemRangeChanged(position, count, payload) + } + } + + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatViewData, newItem: ChatViewData): Boolean { + return oldItem.getViewDataId() == newItem.getViewDataId() + } + + override fun areContentsTheSame(oldItem: ChatViewData, newItem: ChatViewData): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: ChatViewData, newItem: ChatViewData): Any? { + return if (oldItem.deepEquals(newItem)) { + //If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else // If items are different - update a whole view holder + null + } + } + + private val differ = AsyncListDiffer(listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build()) + + private val dataSource = object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): ChatViewData { + return differ.currentList[pos] + } + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + 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 = false, + cardViewMode = CardViewMode.NONE, + confirmReblogs = false, + renderStatusAsMention = false, + hideStats = false + ) + + adapter = ChatsAdapter(dataSource, statusDisplayOptions, this, accountManager.activeAccount!!.accountId) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + bottomSheetActivity = if (context is BottomSheetActivity) { + context + } else { + throw IllegalStateException("Fragment must be attached to a BottomSheetActivity!") + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_timeline, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + swipeRefreshLayout.setOnRefreshListener(this) + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + // TODO: a11y + recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(view.context) + recyclerView.layoutManager = layoutManager + recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + recyclerView.adapter = adapter + + if (chats.isEmpty()) { + progressBar.visibility = View.VISIBLE + bottomLoading = true + sendInitialRequest() + } else { + progressBar.visibility = View.GONE + if (isNeedRefresh) onRefresh() + } + } + private fun sendInitialRequest() { + // debug + // sendFetchChatsRequest(null, null, null, FetchEnd.BOTTOM, -1) + tryCache() + } + + private fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + chatRepo.getChats(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { newChats -> + if (newChats.size > 1) { + val mutableChats = newChats.toMutableList() + mutableChats.removeAll { it.isLeft() } + + chats.clear() + chats.addAll(mutableChats) + + updateAdapter() + progressBar.visibility = View.GONE + } + updateCurrent() + loadAbove() + } + } + + private fun updateCurrent() { + if (!BROKEN_PAGINATION_IN_BACKEND && chats.isEmpty()) { + return + } + + val topId = chats.firstOrNull { it.isRight() }?.asRight()?.id + chatRepo.getChats(topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ newChats -> + initialUpdateFailed = false + // When cached timeline is too old, we would replace it with nothing + if (newChats.isNotEmpty()) { + // clear old cached statuses + if(BROKEN_PAGINATION_IN_BACKEND) { + chats.clear() + } else { + chats.removeAll { + if(it.isLeft()) { + val p = it.asLeft() + p.id.length < topId!!.length || p.id < topId + } else { + val c = it.asRight() + c.id.length < topId!!.length || c.id < topId + } + } + } + chats.addAll(newChats) + updateAdapter() + } + bottomLoading = false + // Indicate that we are not loading anymore + progressBar.visibility = View.GONE + swipeRefreshLayout.isRefreshing = false + }, { + initialUpdateFailed = true + // Indicate that we are not loading anymore + progressBar.visibility = View.GONE + swipeRefreshLayout.isRefreshing = false + }) + } + + private fun showNothing() { + statusView.visibility = View.VISIBLE + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + + private fun removeAllByAccountId(accountId: String) { + chats.removeAll { + val chat = it.asRightOrNull() + chat != null && chat.account.id == accountId + } + updateAdapter() + } + + private fun removeAllByInstance(instance: String) { + chats.removeAll { + val chat = it.asRightOrNull() + chat != null && LinkHelper.getDomain(chat.account.url) == instance + } + updateAdapter() + } + + private fun deleteChatById(id: String) { + val iterator = chats.iterator() + while(iterator.hasNext()) { + val chat = iterator.next().asRightOrNull() + if(chat != null && chat.id == id) { + iterator.remove() + updateAdapter() + break + } + } + + if(chats.isEmpty()) { + showNothing() + } + } + + 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. */ + /* 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) + scrollListener = object : EndlessOnScrollListener(layoutManager) { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(view, dx, dy) + val activity = activity as ActionButtonActivity? + val composeButton = activity!!.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) { + if(!BROKEN_PAGINATION_IN_BACKEND) + this@ChatsFragment.onLoadMore() + } + } + recyclerView.addOnScrollListener(scrollListener) + if (!eventRegistered) { + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when(event) { + is BlockEvent -> removeAllByAccountId(event.accountId) + is MuteEvent -> removeAllByAccountId(event.accountId) + is DomainMuteEvent -> removeAllByInstance(event.instance) + is StatusDeletedEvent -> deleteChatById(event.statusId) + is PreferenceChangedEvent -> onPreferenceChanged(event.preferenceKey) + is ChatMessageReceivedEvent -> onRefresh() // TODO: proper update + } + } + eventRegistered = true + } + } + + /* + private fun onChatMessageReceived(msg: ChatMessage) { + val pos = findChatPosition(msg.chatId) + if(pos == -1) { + + return + } + + val oldChat = chats[pos].asRight() + val newChat = Chat(oldChat.account, oldChat.id, oldChat.unread + 1, msg, msg.createdAt) + val newViewData = ViewDataUtils.chatToViewData(newChat) + + chats.removeAt(pos) + chats.add(pos, newChat.lift()) + chats.sortByDescending { + if(it.isLeft()) Date(Long.MIN_VALUE) + else it.asRight().updatedAt + } + + updateAdapter() + } + */ + + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + when (key) { + "fabHide" -> { + hideFab = sharedPreferences.getBoolean("fabHide", false) + } + } + } + + override fun onRefresh() { + if (isSwipeToRefreshEnabled) + swipeRefreshLayout.isEnabled = true + + statusView.visibility = View.GONE + isNeedRefresh = false + + if (this.initialUpdateFailed) { + updateCurrent() + } + loadAbove() + } + + private fun loadAbove() { + if(BROKEN_PAGINATION_IN_BACKEND) { + updateCurrent() + return + } + + var firstOrNull: String? = null + var secondOrNull: String? = null + for (i in chats.indices) { + val chat = chats[i] + if (chat.isRight()) { + firstOrNull = chat.asRight().id + if (i + 1 < chats.size && chats[i + 1].isRight()) { + secondOrNull = chats[i + 1].asRight().id + } + break + } + } + if (firstOrNull != null) { + sendFetchChatsRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) + } else { + sendFetchChatsRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + private fun onLoadMore() { + if (BROKEN_PAGINATION_IN_BACKEND) + updateCurrent() + return + + if (didLoadEverythingBottom || bottomLoading) { + return + } + if (chats.isEmpty()) { + sendInitialRequest() + return + } + bottomLoading = true + val last = chats.last() + val placeholder: Placeholder + if (last.isRight()) { + val placeholderId = last.asRight().id.dec() + placeholder = Placeholder(placeholderId) + chats.add(Left(placeholder)) + } else { + placeholder = last.asLeft() + } + chats.setPairedItem(chats.size - 1, + ChatViewData.Placeholder(placeholder.id, true)) + updateAdapter() + val bottomId = chats.findLast { it.isRight() }?.let { it.asRight().id } + sendFetchChatsRequest(bottomId, null, null, FetchEnd.BOTTOM, -1) + } + + + private fun sendFetchChatsRequest(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, + fetchEnd: FetchEnd, pos: Int) { + if (isAdded + && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && progressBar.visibility != View.VISIBLE) + && !isSwipeToRefreshEnabled) + topProgressBar.show() + // allow getting old statuses/fallbacks for network only for for bottom loading + val mode = if (fetchEnd == FetchEnd.BOTTOM) { + TimelineRequestMode.ANY + } else { + TimelineRequestMode.NETWORK + } + chatRepo.getChats(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( { result -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, + { onFetchTimelineFailure(Exception(it), fetchEnd, pos) }) + } + + private fun updateChats(newChats: MutableList, fullFetch: Boolean) { + if (newChats.isEmpty()) { + updateAdapter() + return + } + if (chats.isEmpty()) { + chats.addAll(newChats) + } else { + val lastOfNew = newChats[newChats.size - 1] + val index = chats.indexOf(lastOfNew) + if (index >= 0) { + chats.subList(0, index).clear() + } + val newIndex = newChats.indexOf(chats[0]) + if (newIndex == -1) { + if (index == -1 && fullFetch) { + newChats.last { it.isRight() }.let { + val placeholderId = it.asRight().id.inc() + newChats.add(Left(Placeholder(placeholderId))) + } + } + chats.addAll(0, newChats) + } else { + chats.addAll(0, newChats.subList(0, newIndex)) + } + } + // Remove all consecutive placeholders + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun removeConsecutivePlaceholders() { + for (i in 0 until chats.size - 1) { + if (chats[i].isLeft() && chats[i + 1].isLeft()) { + chats.removeAt(i) + } + } + } + + private fun replacePlaceholderWithChats(newChats: MutableList, + fullFetch: Boolean, pos: Int) { + val placeholder = chats[pos] + if (placeholder.isLeft()) { + chats.removeAt(pos) + } + if (newChats.isEmpty()) { + updateAdapter() + return + } + if (fullFetch) { + newChats.add(placeholder) + } + chats.addAll(pos, newChats) + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun addItems(newChats: List) { + if (newChats.isEmpty()) { + return + } + val last = chats.findLast { it.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 && !newChats.contains(last)) { + chats.addAll(newChats) + removeConsecutivePlaceholders() + updateAdapter() + } + } + + private fun onFetchTimelineSuccess(chats: 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 = chats.size >= LOAD_AT_ONCE + + when (fetchEnd) { + FetchEnd.TOP -> { + updateChats(chats, fullFetch) + } + FetchEnd.MIDDLE -> { + replacePlaceholderWithChats(chats, fullFetch, pos) + } + FetchEnd.BOTTOM -> { + if (this.chats.isNotEmpty() && !this.chats.last().isRight()) { + this.chats.removeAt(this.chats.size - 1) + updateAdapter() + } + + if (chats.isNotEmpty() && !chats.last().isRight()) { + // Removing placeholder if it's the last one from the cache + chats.removeAt(chats.size - 1) + } + + val oldSize = this.chats.size + if (this.chats.size > 1) { + addItems(chats) + } else { + updateChats(chats, fullFetch) + } + + if (this.chats.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) { + topProgressBar.hide() + updateBottomLoadingState(fetchEnd) + progressBar.visibility = View.GONE + swipeRefreshLayout.isRefreshing = false + swipeRefreshLayout.isEnabled = true + if (this.chats.size == 0) { + showNothing() + } else { + this.statusView.visibility = View.GONE + } + } + } + + private fun onFetchTimelineFailure(exception: Exception, fetchEnd: FetchEnd, position: Int) { + if (isAdded) { + swipeRefreshLayout.isRefreshing = false + topProgressBar.hide() + if (fetchEnd == FetchEnd.MIDDLE && !chats[position].isRight()) { + var placeholder = chats[position].asLeftOrNull() + val newViewData: ChatViewData + if (placeholder == null) { + val chat = chats[position - 1].asRight() + val newId = chat.id.dec() + placeholder = Placeholder(newId) + } + newViewData = ChatViewData.Placeholder(placeholder.id, false) + chats.setPairedItem(position, newViewData) + updateAdapter() + } else if (chats.isEmpty()) { + swipeRefreshLayout.isEnabled = false + statusView.visibility = View.VISIBLE + if (exception is IOException) { + statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + progressBar.visibility = View.VISIBLE + onRefresh() + } + } else { + statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + progressBar.visibility = View.VISIBLE + onRefresh() + } + } + } + Log.e(TAG, "Fetch Failure: " + exception.message) + updateBottomLoadingState(fetchEnd) + progressBar.visibility = View.GONE + } + } + + private fun updateBottomLoadingState(fetchEnd: FetchEnd) { + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false + } + } + + override fun onLoadMore(position: Int) { + //check bounds before accessing list, + if (chats.size >= position && position > 0) { + val fromChat = chats[position - 1].asRightOrNull() + val toChat = chats[position + 1].asRightOrNull() + if (fromChat == null || toChat == null) { + Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + return + } + + val maxMinusOne = if (chats.size > position + 1 && chats[position + 2].isRight()) chats[position + 1].asRight().id else null + sendFetchChatsRequest(fromChat.id, toChat.id, maxMinusOne, + FetchEnd.MIDDLE, position) + + val (id) = chats[position].asLeft() + val newViewData = ChatViewData.Placeholder(id, true) + chats.setPairedItem(position, newViewData) + updateAdapter() + } else { + Log.e(TAG, "error loading more") + } + } + + override fun onViewAccount(id: String?) { + id?.let(bottomSheetActivity::viewAccount) + } + + override fun onViewUrl(url: String?) { + url?.let { bottomSheetActivity.viewUrl(it, PostLookupFallbackBehavior.OPEN_IN_BROWSER) } + } + + // never called + override fun onViewTag(tag: String?) {} + + private fun updateAdapter() { + Log.d(TAG, "updateAdapter") + differ.submitList(chats.pairedCopy) + } + + private fun jumpToTop() { + if (isAdded) { + layoutManager.scrollToPosition(0) + recyclerView.stopScroll() + scrollListener.reset() + } + } + + override fun onReselect() { + jumpToTop() + } + + override fun onResume() { + super.onResume() + startUpdateTimestamp() + } + + override fun refreshContent() { + if (isAdded) onRefresh() else isNeedRefresh = true + } + + /** + * 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("absoluteTimeView", false) + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_PAUSE) + .subscribe { updateAdapter() } + } + } + + private fun findChatPosition(id: String) : Int { + return chats.indexOfFirst { it.isRight() && it.asRight().id == id } + } + + private fun markAsRead(chat: Chat) { + val pos = findChatPosition(chat.id) + val chatViewData = ViewDataUtils.chatToViewData(chat) + + chats.setPairedItem(pos, chatViewData) + updateAdapter() + } + + override fun onMore(id: String, v: View) { + val popup = PopupMenu(requireContext(), v) + popup.inflate(R.menu.chat_more) + val pos = findChatPosition(id) + val chat = chats[pos].asRight() + // val menu = popup.menu + popup.setOnMenuItemClickListener { + when(it.itemId) { + R.id.chat_mark_as_read -> { + api.markChatAsRead(chat.id, chat.lastMessage?.id ?: null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ chat -> markAsRead(chat) + }, { err -> Log.e(TAG, "Failed to mark chat as read", err) }) + + true + } + else -> { + false // ???? + } + } + } + popup.show() + } + + override fun openChat(position: Int) { + if(position < 0 || position >= chats.size) + return + + val chat = chats[position].asRightOrNull() + chat?.let { + bottomSheetActivity.openChat(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java new file mode 100644 index 0000000..fb765cb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -0,0 +1,1484 @@ +/* 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.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.arch.core.util.Function; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +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; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.NotificationsAdapter; +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; +import com.keylesspalace.tusky.appstore.*; +import com.keylesspalace.tusky.db.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.entity.*; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Poll; +import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.AccountActionListener; +import com.keylesspalace.tusky.interfaces.ActionButtonActivity; +import com.keylesspalace.tusky.interfaces.ReselectableFragment; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +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.ListStatusAccessibilityDelegate; +import com.keylesspalace.tusky.util.ListUtils; +import com.keylesspalace.tusky.util.NotificationTypeConverterKt; +import com.keylesspalace.tusky.util.PairedList; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.ViewDataUtils; +import com.keylesspalace.tusky.view.BackgroundMessageView; +import com.keylesspalace.tusky.view.EndlessOnScrollListener; +import com.keylesspalace.tusky.viewdata.NotificationViewData; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +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 okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static com.keylesspalace.tusky.util.StringUtils.isLessThan; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + +public class NotificationsFragment extends SFragment implements + SwipeRefreshLayout.OnRefreshListener, + StatusActionListener, + NotificationsAdapter.NotificationActionListener, + AccountActionListener, + Injectable, ReselectableFragment { + private static final String TAG = "NotificationF"; // logging tag + + private static final int LOAD_AT_ONCE = 30; + private int maxPlaceholderId = 0; + + + private Set notificationFilter = new HashSet<>(); + + private enum FetchEnd { + TOP, + BOTTOM, + MIDDLE + } + + /** + * Placeholder for the notificationsEnabled. Consider moving to the separate class to hide constructor + * and reuse in different places as needed. + */ + private static final class Placeholder { + final long id; + + public static Placeholder getInstance(long id) { + return new Placeholder(id); + } + + private Placeholder(long id) { + this.id = id; + } + } + + @Inject + AccountManager accountManager; + @Inject + EventHub eventHub; + + private SwipeRefreshLayout swipeRefreshLayout; + private RecyclerView recyclerView; + private ProgressBar progressBar; + private BackgroundMessageView statusView; + private AppBarLayout appBarOptions; + + private LinearLayoutManager layoutManager; + private EndlessOnScrollListener scrollListener; + private NotificationsAdapter adapter; + private Button buttonFilter; + private boolean hideFab; + private boolean topLoading; + private boolean bottomLoading; + private String bottomId; + private boolean alwaysShowSensitiveMedia; + private boolean alwaysOpenSpoiler; + private boolean showNotificationsFilter; + private boolean showingError; + private boolean withMuted; + + // Each element is either a Notification for loading data or a Placeholder + private final PairedList, NotificationViewData> notifications + = new PairedList<>(new Function, NotificationViewData>() { + @Override + public NotificationViewData apply(Either input) { + if (input.isRight()) { + Notification notification = Notification.rewriteToStatusTypeIfNeeded( + input.asRight(), accountManager.getActiveAccount().getAccountId() + ); + + return ViewDataUtils.notificationToViewData( + notification, + alwaysShowSensitiveMedia, + alwaysOpenSpoiler + ); + } else { + return new NotificationViewData.Placeholder(input.asLeft().id, false); + } + } + }); + + public static NotificationsFragment newInstance() { + NotificationsFragment fragment = new NotificationsFragment(); + Bundle arguments = new Bundle(); + fragment.setArguments(arguments); + return fragment; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_timeline_notifications, container, false); + + @NonNull Context context = inflater.getContext(); // from inflater to silence warning + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + boolean showNotificationsFilterSetting = preferences.getBoolean("showNotificationsFilter", true); + //Clear notifications on filter visibility change to force refresh + if (showNotificationsFilterSetting != showNotificationsFilter) + notifications.clear(); + showNotificationsFilter = showNotificationsFilterSetting; + + // Setup the SwipeRefreshLayout. + swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); + recyclerView = rootView.findViewById(R.id.recyclerView); + progressBar = rootView.findViewById(R.id.progressBar); + statusView = rootView.findViewById(R.id.statusView); + appBarOptions = rootView.findViewById(R.id.appBarOptions); + + swipeRefreshLayout.setOnRefreshListener(this); + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); + + loadNotificationsFilter(); + + // Setup the RecyclerView. + recyclerView.setHasFixedSize(true); + layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAccessibilityDelegateCompat( + new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> { + NotificationViewData notification = notifications.getPairedItemOrNull(pos); + // We support replies only for now + if (notification instanceof NotificationViewData.Concrete) { + return ((NotificationViewData.Concrete) notification).getStatusViewData(); + } else { + return null; + } + })); + + recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); + + StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( + preferences.getBoolean("animateGifAvatars", false), + accountManager.getActiveAccount().getMediaPreviewEnabled(), + preferences.getBoolean("absoluteTimeView", false), + preferences.getBoolean("showBotOverlay", true), + preferences.getBoolean("useBlurhash", true), + CardViewMode.NONE, + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + ); + withMuted = !preferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); + + adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), + dataSource, statusDisplayOptions, this, this, this); + alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); + alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); + recyclerView.setAdapter(adapter); + + topLoading = false; + bottomLoading = false; + bottomId = null; + + updateAdapter(); + + Button buttonClear = rootView.findViewById(R.id.buttonClear); + buttonClear.setOnClickListener(v -> confirmClearNotifications()); + buttonFilter = rootView.findViewById(R.id.buttonFilter); + buttonFilter.setOnClickListener(v -> showFilterMenu()); + + if (notifications.isEmpty()) { + swipeRefreshLayout.setEnabled(false); + sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1); + } else { + progressBar.setVisibility(View.GONE); + } + + ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); + + updateFilterVisibility(); + + return rootView; + } + + private void updateFilterVisibility() { + CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + if (showNotificationsFilter && !showingError) { + appBarOptions.setExpanded(true, false); + appBarOptions.setVisibility(View.VISIBLE); + //Set content behaviour to hide filter on scroll + params.setBehavior(new AppBarLayout.ScrollingViewBehavior()); + } else { + appBarOptions.setExpanded(false, false); + appBarOptions.setVisibility(View.GONE); + //Clear behaviour to hide app bar + params.setBehavior(null); + } + } + + private void confirmClearNotifications() { + new AlertDialog.Builder(getContext()) + .setMessage(R.string.notification_clear_text) + .setPositiveButton(android.R.string.yes, (DialogInterface dia, int which) -> clearNotifications()) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + private void handleFavEvent(FavoriteEvent event) { + Pair posAndNotification = + findReplyPosition(event.getStatusId()); + if (posAndNotification == null) return; + //noinspection ConstantConditions + setFavouriteForStatus(posAndNotification.first, + posAndNotification.second.getStatus(), + event.getFavourite()); + } + + private void handleBookmarkEvent(BookmarkEvent event) { + Pair posAndNotification = + findReplyPosition(event.getStatusId()); + if (posAndNotification == null) return; + //noinspection ConstantConditions + setBookmarkForStatus(posAndNotification.first, + posAndNotification.second.getStatus(), + event.getBookmark()); + } + + private void handleReblogEvent(ReblogEvent event) { + Pair posAndNotification = findReplyPosition(event.getStatusId()); + if (posAndNotification == null) return; + //noinspection ConstantConditions + setReblogForStatus(posAndNotification.first, + posAndNotification.second.getStatus(), + event.getReblog()); + } + + private void handleMuteStatusEvent(MuteConversationEvent event) { + Pair posAndNotification = findReplyPosition(event.getStatusId()); + if (posAndNotification == null) + return; + + int conversationId = posAndNotification.second.getStatus().getConversationId(); + + if(conversationId == -1) { // invalid conversation ID + if(withMuted) { + setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute(), event.getMute()); + } else { + notifications.remove(posAndNotification.first); + } + } else { + //noinspection ConstantConditions + if(withMuted) { + for (int i = 0; i < notifications.size(); i++) { + Notification notification = notifications.get(i).asRightOrNull(); + if (notification != null && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION && + notification.getStatus().getConversationId() == conversationId) { + setMutedStatusForStatus(i, notification.getStatus(), event.getMute(), event.getMute()); + } + } + } else { + removeAllByConversationId(conversationId); + } + } + updateAdapter(); + } + + private void handleMuteEvent(MuteEvent event) { + String id = event.getAccountId(); + boolean mute = event.getMute(); + + if(withMuted) { + for (int i = 0; i < notifications.size(); i++) { + Notification notification = notifications.get(i).asRightOrNull(); + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && notification.getAccount().getId().equals(id) + && !notification.getStatus().isThreadMuted()) { + setMutedStatusForStatus(i, notification.getStatus(), mute, false); + } + } + updateAdapter(); + } else { + removeAllByAccountId(id); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Activity activity = getActivity(); + if (activity == null) throw new AssertionError("Activity is null"); + + /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't + * guaranteed to be set until then. + * Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides + * the compose button on down-scroll. */ + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + hideFab = preferences.getBoolean("fabHide", false); + scrollListener = new EndlessOnScrollListener(layoutManager) { + @Override + public void onScrolled(@NonNull 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) { + NotificationsFragment.this.onLoadMore(); + } + }; + + recyclerView.addOnScrollListener(scrollListener); + + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe(event -> { + if (event instanceof FavoriteEvent) { + handleFavEvent((FavoriteEvent) event); + } else if (event instanceof BookmarkEvent) { + handleBookmarkEvent((BookmarkEvent) event); + } else if (event instanceof ReblogEvent) { + handleReblogEvent((ReblogEvent) event); + } else if (event instanceof MuteConversationEvent) { + handleMuteStatusEvent((MuteConversationEvent) event); + } else if (event instanceof BlockEvent) { + removeAllByAccountId(((BlockEvent) event).getAccountId()); + } else if (event instanceof MuteEvent) { + handleMuteEvent((MuteEvent)event); + } else if (event instanceof PreferenceChangedEvent) { + onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey()); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent) event); + } + }); + } + + @Override + public void onRefresh() { + this.statusView.setVisibility(View.GONE); + this.showingError = false; + Either first = CollectionsKt.firstOrNull(this.notifications); + String topId; + if (first != null && first.isRight()) { + topId = first.asRight().getId(); + } else { + topId = null; + } + sendFetchNotificationsRequest(null, topId, FetchEnd.TOP, -1); + } + + @Override + public void onReply(int position) { + super.reply(notifications.get(position).asRight().getStatus()); + } + + @Override + public void onReblog(final boolean reblog, final int position) { + final Notification notification = notifications.get(position).asRight(); + final Status status = notification.getStatus(); + Objects.requireNonNull(status, "Reblog on notification without status"); + timelineCases.reblog(status, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setReblogForStatus(position, status, reblog), + (t) -> Log.d(getClass().getSimpleName(), + "Failed to reblog status: " + status.getId(), t) + ); + } + + private void setReblogForStatus(int position, Status status, boolean reblog) { + status.setReblogged(reblog); + + if (status.getReblog() != null) { + status.getReblog().setReblogged(reblog); + } + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setReblogged(reblog); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), viewdata.getTarget()); + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + + @Override + public void onFavourite(final boolean favourite, final int position) { + final Notification notification = notifications.get(position).asRight(); + final Status status = notification.getStatus(); + + timelineCases.favourite(status, favourite) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setFavouriteForStatus(position, status, favourite), + (t) -> Log.d(getClass().getSimpleName(), + "Failed to favourite status: " + status.getId(), t) + ); + } + + private void setFavouriteForStatus(int position, Status status, boolean favourite) { + status.setFavourited(favourite); + + if (status.getReblog() != null) { + status.getReblog().setFavourited(favourite); + } + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setFavourited(favourite); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), viewdata.getTarget()); + + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + + @Override + public void onBookmark(final boolean bookmark, final int position) { + final Notification notification = notifications.get(position).asRight(); + final Status status = notification.getStatus(); + + timelineCases.bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setBookmarkForStatus(position, status, bookmark), + (t) -> Log.d(getClass().getSimpleName(), + "Failed to bookmark status: " + status.getId(), t) + ); + } + + private void setBookmarkForStatus(int position, Status status, boolean bookmark) { + status.setBookmarked(bookmark); + + if (status.getReblog() != null) { + status.getReblog().setBookmarked(bookmark); + } + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setBookmarked(bookmark); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), viewdata.getTarget()); + + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + + public void onVoteInPoll(int position, @NonNull List choices) { + final Notification notification = notifications.get(position).asRight(); + final Status status = notification.getStatus(); + + timelineCases.voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newPoll) -> setVoteForPoll(position, newPoll), + (t) -> Log.d(TAG, + "Failed to vote in poll: " + status.getId(), t) + ); + } + + private void setVoteForPoll(int position, Poll poll) { + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setPoll(poll); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), viewdata.getTarget()); + + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + + @Override + public void onMore(@NonNull View view, int position) { + Notification notification = notifications.get(position).asRight(); + super.more(notification.getStatus(), view, position); + } + + @Override + public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { + Notification notification = notifications.get(position).asRightOrNull(); + if (notification == null || notification.getStatus() == null) return; + super.viewMedia(attachmentIndex, notification.getStatus(), view); + } + + @Override + public void onViewThread(int position) { + Notification notification = notifications.get(position).asRight(); + super.viewThread(notification.getStatus()); + } + + @Override + public void onViewReplyTo(int position) { + Notification notification = notifications.get(position).asRightOrNull(); + if (notification == null) return; + super.onShowReplyTo(notification.getStatus().getInReplyToId()); + } + + @Override + public void onOpenReblog(int position) { + Notification notification = notifications.get(position).asRight(); + onViewAccount(notification.getAccount().getId()); + } + + @Override + public void onExpandedChange(boolean expanded, int position) { + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder(old.getStatusViewData()) + .setIsExpanded(expanded) + .createStatusViewData(); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData, old.getEmoji(), old.getTarget()); + notifications.setPairedItem(position, notificationViewData); + updateAdapter(); + } + + @Override + public void onContentHiddenChange(boolean isShowing, int position) { + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder(old.getStatusViewData()) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData, old.getEmoji(), old.getTarget()); + notifications.setPairedItem(position, notificationViewData); + updateAdapter(); + } + + @Override + public void onMute(int position, boolean isMuted) { + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder(old.getStatusViewData()) + .setMuted(isMuted) + .createStatusViewData(); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData, old.getEmoji(), old.getTarget()); + notifications.setPairedItem(position, notificationViewData); + updateAdapter(); + } + + private void setMutedStatusForStatus(int position, Status status, boolean muted, boolean threadMuted) { + status.setThreadMuted(threadMuted); + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setThreadMuted(threadMuted); + viewDataBuilder.setMuted(muted); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), viewdata.getTarget()); + + notifications.setPairedItem(position, newViewData); + } + + + @Override + public void onLoadMore(int position) { + //check bounds before accessing list, + if (notifications.size() >= position && position > 0) { + Notification previous = notifications.get(position - 1).asRightOrNull(); + Notification next = notifications.get(position + 1).asRightOrNull(); + if (previous == null || next == null) { + Log.e(TAG, "Failed to load more, invalid placeholder position: " + position); + return; + } + sendFetchNotificationsRequest(previous.getId(), next.getId(), FetchEnd.MIDDLE, position); + Placeholder placeholder = notifications.get(position).asLeft(); + NotificationViewData notificationViewData = + new NotificationViewData.Placeholder(placeholder.id, true); + notifications.setPairedItem(position, notificationViewData); + updateAdapter(); + } else { + Log.d(TAG, "error loading more"); + } + } + + @Override + public void onContentCollapsedChange(boolean isCollapsed, int position) { + if (position < 0 || position >= notifications.size()) { + Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, notifications.size() - 1)); + return; + } + + NotificationViewData notification = notifications.getPairedItem(position); + if (!(notification instanceof NotificationViewData.Concrete)) { + Log.e(TAG, String.format( + "Expected NotificationViewData.Concrete, got %s instead at position: %d of %d", + notification == null ? "null" : notification.getClass().getSimpleName(), + position, + notifications.size() - 1 + )); + return; + } + + StatusViewData.Concrete status = ((NotificationViewData.Concrete) notification).getStatusViewData(); + StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status) + .setCollapsed(isCollapsed) + .createStatusViewData(); + + NotificationViewData.Concrete concreteNotification = (NotificationViewData.Concrete) notification; + NotificationViewData updatedNotification = new NotificationViewData.Concrete( + concreteNotification.getType(), + concreteNotification.getId(), + concreteNotification.getAccount(), + updatedStatus, + concreteNotification.getEmoji(), + concreteNotification.getTarget() + ); + notifications.setPairedItem(position, updatedNotification); + updateAdapter(); + + // Since we cannot notify to the RecyclerView right away because it may be scrolling + // we run this when the RecyclerView is done doing measurements and other calculations. + // To test this is not bs: try getting a notification while scrolling, without wrapping + // notifyItemChanged in a .post() call. App will crash. + recyclerView.post(() -> adapter.notifyItemChanged(position, notification)); + } + + @Override + public void onNotificationContentCollapsedChange(boolean isCollapsed, int position) { + onContentCollapsedChange(isCollapsed, position); + } + + private void clearNotifications() { + //Cancel all ongoing requests + swipeRefreshLayout.setRefreshing(false); + resetNotificationsLoad(); + + //Show friend elephant + this.statusView.setVisibility(View.VISIBLE); + this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + updateFilterVisibility(); + + //Update adapter + updateAdapter(); + + //Execute clear notifications request + Call call = mastodonApi.clearNotifications(); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (isAdded()) { + if (!response.isSuccessful()) { + //Reload notifications on failure + fullyRefreshWithProgressBar(true); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + //Reload notifications on failure + fullyRefreshWithProgressBar(true); + } + }); + callList.add(call); + } + + private void resetNotificationsLoad() { + for (Call callItem : callList) { + callItem.cancel(); + } + callList.clear(); + bottomLoading = false; + topLoading = false; + + //Disable load more + bottomId = null; + + //Clear exists notifications + notifications.clear(); + } + + + private void showFilterMenu() { + List notificationsList = Notification.Type.Companion.getAsList(); + List list = new ArrayList<>(); + for (Notification.Type type : notificationsList) { + // ignore chat messages, as we don't work with them in main notification fragment + if(type == Notification.Type.CHAT_MESSAGE) + continue; + + list.add(getNotificationText(type)); + } + + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_multiple_choice, list); + PopupWindow window = new PopupWindow(getContext()); + View view = LayoutInflater.from(getContext()).inflate(R.layout.notifications_filter, (ViewGroup) getView(), false); + final ListView listView = view.findViewById(R.id.listView); + view.findViewById(R.id.buttonApply) + .setOnClickListener(v -> { + SparseBooleanArray checkedItems = listView.getCheckedItemPositions(); + Set excludes = new HashSet<>(); + for (int i = 0; i < notificationsList.size(); i++) { + if (!checkedItems.get(i, false)) + excludes.add(notificationsList.get(i)); + } + window.dismiss(); + applyFilterChanges(excludes); + + }); + + listView.setAdapter(adapter); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + for (int i = 0; i < notificationsList.size(); i++) { + if (!notificationFilter.contains(notificationsList.get(i))) + listView.setItemChecked(i, true); + } + window.setContentView(view); + window.setFocusable(true); + window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + window.showAsDropDown(buttonFilter); + + } + + private String getNotificationText(Notification.Type type) { + switch (type) { + case MENTION: + return getString(R.string.notification_mention_name); + case FAVOURITE: + return getString(R.string.notification_favourite_name); + case REBLOG: + return getString(R.string.notification_boost_name); + case FOLLOW: + return getString(R.string.notification_follow_name); + case FOLLOW_REQUEST: + return getString(R.string.notification_follow_request_name); + case POLL: + return getString(R.string.notification_poll_name); + case EMOJI_REACTION: + return getString(R.string.notification_emoji_name); + case MOVE: + return getString(R.string.notification_move_name); + case STATUS: + return getString(R.string.notification_subscription_name); + default: + return "Unknown"; + } + } + + private void applyFilterChanges(Set newSet) { + List notifications = Notification.Type.Companion.getAsList(); + boolean isChanged = false; + for (Notification.Type type : notifications) { + if (notificationFilter.contains(type) && !newSet.contains(type)) { + notificationFilter.remove(type); + isChanged = true; + } else if (!notificationFilter.contains(type) && newSet.contains(type)) { + notificationFilter.add(type); + isChanged = true; + } + } + if (isChanged) { + saveNotificationsFilter(); + fullyRefreshWithProgressBar(true); + } + + } + + private void loadNotificationsFilter() { + AccountEntity account = accountManager.getActiveAccount(); + if (account != null) { + notificationFilter.clear(); + notificationFilter.addAll(NotificationTypeConverterKt.deserialize( + account.getNotificationsFilter())); + } + } + + private void saveNotificationsFilter() { + AccountEntity account = accountManager.getActiveAccount(); + if (account != null) { + account.setNotificationsFilter(NotificationTypeConverterKt.serialize(notificationFilter)); + accountManager.saveAccount(account); + } + } + + @Override + public void onViewTag(String tag) { + super.viewTag(tag); + } + + @Override + public void onViewAccount(String id) { + super.viewAccount(id); + } + + @Override + public void onMute(boolean mute, String id, int position, boolean notifications) { + // No muting from notifications yet + } + + @Override + public void onBlock(boolean block, String id, int position) { + // No blocking from notifications yet + } + + @Override + public void onRespondToFollowRequest(boolean accept, String id, int position) { + Single request = accept ? + mastodonApi.authorizeFollowRequestObservable(id) : + mastodonApi.rejectFollowRequestObservable(id); + request.observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (relationship) -> fullyRefreshWithProgressBar(true), + (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id)) + ); + } + + @Override + public void onViewStatusForNotificationId(String notificationId) { + for (Either either : notifications) { + Notification notification = either.asRightOrNull(); + if (notification != null && notification.getId().equals(notificationId)) { + super.viewThread(notification.getStatus()); + return; + } + } + Log.w(TAG, "Didn't find a notification for ID: " + notificationId); + } + + 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(); + if (enabled != adapter.isMediaPreviewEnabled()) { + adapter.setMediaPreviewEnabled(enabled); + fullyRefresh(); + } + } + case "showNotificationsFilter": { + if (isAdded()) { + showNotificationsFilter = sharedPreferences.getBoolean("showNotificationsFilter", true); + updateFilterVisibility(); + fullyRefreshWithProgressBar(true); + } + } + case PrefKeys.HIDE_MUTED_USERS: { + withMuted = !sharedPreferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); + fullyRefresh(); + } + } + } + + @Override + public void removeItem(int position) { + notifications.remove(position); + updateAdapter(); + } + + private void removeAllByConversationId(int conversationId) { + // using iterator to safely remove items while iterating + Iterator> iterator = notifications.iterator(); + while (iterator.hasNext()) { + Either placeholderOrNotification = iterator.next(); + Notification notification = placeholderOrNotification.asRightOrNull(); + if (notification != null && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION && + notification.getStatus().getConversationId() == conversationId) { + iterator.remove(); + } + } + updateAdapter(); + } + + private void removeAllByAccountId(String accountId) { + // using iterator to safely remove items while iterating + Iterator> iterator = notifications.iterator(); + while (iterator.hasNext()) { + Either notification = iterator.next(); + Notification maybeNotification = notification.asRightOrNull(); + if (maybeNotification != null && maybeNotification.getAccount().getId().equals(accountId)) { + iterator.remove(); + } + } + updateAdapter(); + } + + private void onLoadMore() { + if (bottomId == null) { + // already loaded everything + return; + } + + // Check for out-of-bounds when loading + // This is required to allow full-timeline reloads of collapsible statuses when the settings + // change. + if (notifications.size() > 0) { + Either last = notifications.get(notifications.size() - 1); + if (last.isRight()) { + final Placeholder placeholder = newPlaceholder(); + notifications.add(new Either.Left<>(placeholder)); + NotificationViewData viewData = + new NotificationViewData.Placeholder(placeholder.id, true); + notifications.setPairedItem(notifications.size() - 1, viewData); + updateAdapter(); + } + } + + sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1); + } + + private Placeholder newPlaceholder() { + Placeholder placeholder = Placeholder.getInstance(maxPlaceholderId); + maxPlaceholderId--; + return placeholder; + } + + private void jumpToTop() { + if (isAdded()) { + appBarOptions.setExpanded(true, false); + layoutManager.scrollToPosition(0); + scrollListener.reset(); + } + } + + private void sendFetchNotificationsRequest(String fromId, String uptoId, + final FetchEnd fetchEnd, final int pos) { + /* If there is a fetch already ongoing, record however many fetches are requested and + * fulfill them after it's complete. */ + if (fetchEnd == FetchEnd.TOP && topLoading) { + return; + } + if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) { + return; + } + if (fetchEnd == FetchEnd.TOP) { + topLoading = true; + } + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = true; + } + + Call> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null, withMuted); + + call.enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful()) { + String linkHeader = response.headers().get("Link"); + onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); + } else { + onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + if (!call.isCanceled()) + onFetchNotificationsFailure((Exception) t, fetchEnd, pos); + } + }); + callList.add(call); + } + + private void onFetchNotificationsSuccess(List notifications, String linkHeader, + FetchEnd fetchEnd, int pos) { + List links = HttpHeaderLink.parse(linkHeader); + HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); + String fromId = null; + if (next != null) { + fromId = next.uri.getQueryParameter("max_id"); + } + + switch (fetchEnd) { + case TOP: { + update(notifications, this.notifications.isEmpty() ? fromId : null); + break; + } + case MIDDLE: { + replacePlaceholderWithNotifications(notifications, pos); + break; + } + case BOTTOM: { + + if (!this.notifications.isEmpty() + && !this.notifications.get(this.notifications.size() - 1).isRight()) { + this.notifications.remove(this.notifications.size() - 1); + updateAdapter(); + } + + if (adapter.getItemCount() > 1) { + addItems(notifications, fromId); + } else { + update(notifications, fromId); + } + + break; + } + } + + saveNewestNotificationId(notifications); + + if (fetchEnd == FetchEnd.TOP) { + topLoading = false; + } + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false; + } + + if (notifications.size() == 0 && adapter.getItemCount() == 0) { + this.statusView.setVisibility(View.VISIBLE); + this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + } else { + swipeRefreshLayout.setEnabled(true); + } + updateFilterVisibility(); + swipeRefreshLayout.setRefreshing(false); + progressBar.setVisibility(View.GONE); + } + + private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { + swipeRefreshLayout.setRefreshing(false); + if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { + Placeholder placeholder = notifications.get(position).asLeft(); + NotificationViewData placeholderVD = + new NotificationViewData.Placeholder(placeholder.id, false); + notifications.setPairedItem(position, placeholderVD); + updateAdapter(); + } else if (this.notifications.isEmpty()) { + this.statusView.setVisibility(View.VISIBLE); + swipeRefreshLayout.setEnabled(false); + this.showingError = true; + if (exception 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; + }); + } + updateFilterVisibility(); + } + Log.e(TAG, "Fetch failure: " + exception.getMessage()); + + if (fetchEnd == FetchEnd.TOP) { + topLoading = false; + } + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false; + } + + progressBar.setVisibility(View.GONE); + } + + private void saveNewestNotificationId(List notifications) { + + AccountEntity account = accountManager.getActiveAccount(); + if (account != null) { + String lastNotificationId = account.getLastNotificationId(); + + for (Notification noti : notifications) { + if (isLessThan(lastNotificationId, noti.getId())) { + lastNotificationId = noti.getId(); + } + } + + if (!account.getLastNotificationId().equals(lastNotificationId)) { + Log.d(TAG, "saving newest noti id: " + lastNotificationId); + account.setLastNotificationId(lastNotificationId); + accountManager.saveAccount(account); + } + } + } + + private void update(@Nullable List newNotifications, @Nullable String fromId) { + if (ListUtils.isEmpty(newNotifications)) { + updateAdapter(); + return; + } + if (fromId != null) { + bottomId = fromId; + } + List> liftedNew = + liftNotificationList(newNotifications); + if (notifications.isEmpty()) { + notifications.addAll(liftedNew); + } else { + int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1)); + for (int i = 0; i < index; i++) { + notifications.remove(0); + } + + int newIndex = liftedNew.indexOf(notifications.get(0)); + if (newIndex == -1) { + if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) { + liftedNew.add(new Either.Left<>(newPlaceholder())); + } + notifications.addAll(0, liftedNew); + } else { + notifications.addAll(0, liftedNew.subList(0, newIndex)); + } + } + updateAdapter(); + } + + private void addItems(List newNotifications, @Nullable String fromId) { + bottomId = fromId; + if (ListUtils.isEmpty(newNotifications)) { + return; + } + int end = notifications.size(); + List> liftedNew = liftNotificationList(newNotifications); + Either last = notifications.get(end - 1); + if (last != null && liftedNew.indexOf(last) == -1) { + notifications.addAll(liftedNew); + updateAdapter(); + } + } + + private void replacePlaceholderWithNotifications(List newNotifications, int pos) { + // Remove placeholder + notifications.remove(pos); + + if (ListUtils.isEmpty(newNotifications)) { + updateAdapter(); + return; + } + + List> liftedNew = liftNotificationList(newNotifications); + + // If we fetched less posts than in the limit, it means that the hole is not filled + // If we fetched at least as much it means that there are more posts to load and we should + // insert new placeholder + if (newNotifications.size() >= LOAD_AT_ONCE) { + liftedNew.add(new Either.Left<>(newPlaceholder())); + } + + notifications.addAll(pos, liftedNew); + updateAdapter(); + } + + private final Function1> notificationLifter = + Either.Right::new; + + private List> liftNotificationList(List list) { + return CollectionsKt.map(list, notificationLifter); + } + + private void fullyRefreshWithProgressBar(boolean isShow) { + resetNotificationsLoad(); + if (isShow) { + progressBar.setVisibility(View.VISIBLE); + statusView.setVisibility(View.GONE); + } + updateAdapter(); + sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1); + } + + private void fullyRefresh() { + fullyRefreshWithProgressBar(false); + } + + @Nullable + private Pair findReplyPosition(@NonNull String statusId) { + for (int i = 0; i < notifications.size(); i++) { + Notification notification = notifications.get(i).asRightOrNull(); + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && (statusId.equals(notification.getStatus().getId()) + || (notification.getStatus().getReblog() != null + && statusId.equals(notification.getStatus().getReblog().getId())))) { + return new Pair<>(i, notification); + } + } + return null; + } + + private void updateAdapter() { + differ.submitList(notifications.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 at the start + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.getItemCount() != count) { + recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); + } + } + } + + @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 NotificationsAdapter.AdapterDataSource dataSource = + new NotificationsAdapter.AdapterDataSource() { + @Override + public int getItemCount() { + return differ.getCurrentList().size(); + } + + @Override + public NotificationViewData getItemAt(int pos) { + return differ.getCurrentList().get(pos); + } + }; + + private static final DiffUtil.ItemCallback diffCallback + = new DiffUtil.ItemCallback() { + + @Override + public boolean areItemsTheSame(NotificationViewData oldItem, NotificationViewData newItem) { + return oldItem.getViewDataId() == newItem.getViewDataId(); + } + + @Override + public boolean areContentsTheSame(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) { + return false; + } + + @Nullable + @Override + public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData 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; + } + }; + + @Override + public void onResume() { + super.onResume(); + String rawAccountNotificationFilter = accountManager.getActiveAccount().getNotificationsFilter(); + Set accountNotificationFilter = NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter); + if (!notificationFilter.equals(accountNotificationFilter)) { + loadNotificationsFilter(); + fullyRefreshWithProgressBar(true); + } + 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(); + } + + private void setEmojiReactForStatus(int position, Status newStatus) { + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + ViewDataUtils.statusToViewData(newStatus, false, false), + viewdata.getEmoji(), viewdata.getTarget()); + + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + + private void handleEmojiReactEvent(EmojiReactEvent event) { + Pair posAndNotification = + findReplyPosition(event.getNewStatus().getActionableId()); + if (posAndNotification == null) return; + //noinspection ConstantConditions + setEmojiReactForStatus(posAndNotification.first, event.getNewStatus()); + } + + + @Override + public void onEmojiReact(final boolean react, final String emoji, final String statusId) { + Pair posAndNotification = findReplyPosition(statusId); + if (posAndNotification == null) + return; + + timelineCases.react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactForStatus(posAndNotification.first, newStatus), + (t) -> Log.d(TAG, + "Failed to react with " + emoji + " on status: " + statusId, t) + ); + + } + + @Override + public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) { + super.emojiReactMenu(statusId, emoji, view, this); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java new file mode 100644 index 0000000..a5baf0e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -0,0 +1,655 @@ +/* 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.Manifest; +import android.app.DownloadManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.view.ViewCompat; +import androidx.lifecycle.Lifecycle; +import androidx.preference.PreferenceManager; + +import com.keylesspalace.tusky.BaseActivity; +import com.keylesspalace.tusky.BottomSheetActivity; +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.AccountListActivity; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.PostLookupFallbackBehavior; +import com.keylesspalace.tusky.ViewMediaActivity; +import com.keylesspalace.tusky.ViewTagActivity; +import com.keylesspalace.tusky.components.compose.ComposeActivity; +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; +import com.keylesspalace.tusky.components.report.ReportActivity; +import com.keylesspalace.tusky.db.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Filter; +import com.keylesspalace.tusky.entity.PollOption; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.network.MastodonApi; +import com.keylesspalace.tusky.network.TimelineCases; +import com.keylesspalace.tusky.settings.PrefKeys; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.view.MuteAccountDialog; +import com.keylesspalace.tusky.viewdata.AttachmentViewData; +import com.keylesspalace.tusky.interfaces.StatusActionListener; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import kotlin.Unit; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + +/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an + * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature + * of that is complicated by how they're coupled with Status and Notification and the corresponding + * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also + * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear + * up what needs to be where. */ +public abstract class SFragment extends BaseFragment implements Injectable { + + protected abstract void removeItem(int position); + + protected abstract void onReblog(final boolean reblog, final int position); + + private BottomSheetActivity bottomSheetActivity; + + private static List filters; + private boolean filterRemoveRegex; + private Matcher filterRemoveRegexMatcher; + private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); + private boolean filterMuted; + + @Inject + public MastodonApi mastodonApi; + @Inject + public AccountManager accountManager; + @Inject + public TimelineCases timelineCases; + + private static final String TAG = "SFragment"; + + @Override + public void startActivity(Intent intent) { + super.startActivity(intent); + getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof BottomSheetActivity) { + bottomSheetActivity = (BottomSheetActivity) context; + } else { + throw new IllegalStateException("Fragment must be attached to a BottomSheetActivity!"); + } + } + + protected void openReblog(@Nullable final Status status) { + if (status == null) return; + bottomSheetActivity.viewAccount(status.getAccount().getId()); + } + + protected void viewThread(Status status) { + Status actionableStatus = status.getActionableStatus(); + bottomSheetActivity.viewThread(actionableStatus.getId(), actionableStatus.getUrl()); + } + + protected void viewAccount(String accountId) { + bottomSheetActivity.viewAccount(accountId); + } + + public void onViewUrl(String url) { + bottomSheetActivity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER); + } + + protected void onShowReplyTo(String replyToId) { + bottomSheetActivity.viewThread(replyToId, null); + } + + protected void reply(Status status) { + String inReplyToId = status.getActionableId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + Status.Mention[] mentions = actionableStatus.getMentions(); + Set mentionedUsernames = new LinkedHashSet<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + String loggedInUsername = null; + AccountEntity activeAccount = accountManager.getActiveAccount(); + if (activeAccount != null) { + loggedInUsername = activeAccount.getUsername(); + } + for (Status.Mention mention : mentions) { + mentionedUsernames.add(mention.getUsername()); + } + mentionedUsernames.remove(loggedInUsername); + ComposeOptions composeOptions = new ComposeOptions(); + composeOptions.setInReplyToId(inReplyToId); + composeOptions.setReplyVisibility(replyVisibility); + composeOptions.setContentWarning(contentWarning); + composeOptions.setMentionedUsernames(mentionedUsernames); + composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername()); + composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString()); + + Intent intent = ComposeActivity.startIntent(getContext(), composeOptions); + getActivity().startActivity(intent); + } + + protected void emojiReactMenu(@NonNull final String statusId, @NonNull final EmojiReaction reaction, View view, final StatusActionListener listener) { + PopupMenu popup = new PopupMenu(getContext(), view); + + popup.inflate(R.menu.emoji_reaction_more); + Menu menu = popup.getMenu(); + menu.findItem(R.id.emoji_react).setVisible(!reaction.getMe()); + menu.findItem(R.id.emoji_unreact).setVisible(reaction.getMe()); + + popup.setOnMenuItemClickListener(item -> { + switch (item.getItemId()) { + case R.id.emoji_react: + listener.onEmojiReact(true, reaction.getName(), statusId); + return true; + case R.id.emoji_unreact: + listener.onEmojiReact(false, reaction.getName(), statusId); + return true; + case R.id.emoji_reacted_by: + Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REACTED, statusId, reaction.getName()); + ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); + + return true; + } + return false; + }); + popup.show(); + } + + protected void more(@NonNull final Status status, View view, final int position) { + final String id = status.getActionableId(); + final String accountId = status.getActionableStatus().getAccount().getId(); + final String accountUsername = status.getActionableStatus().getAccount().getUsername(); + final String statusUrl = status.getActionableStatus().getUrl(); + List accounts = accountManager.getAllAccountsOrderedByActive(); + String openAsTitle = null; + + String loggedInAccountId = null; + AccountEntity activeAccount = accountManager.getActiveAccount(); + if (activeAccount != null) { + loggedInAccountId = activeAccount.getAccountId(); + } + + PopupMenu popup = new PopupMenu(getContext(), view); + // Give a different menu depending on whether this is the user's own toot or not. + if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { + popup.inflate(R.menu.status_more); + Menu menu = popup.getMenu(); + menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty()); + } else { + popup.inflate(R.menu.status_more_for_user); + Menu menu = popup.getMenu(); + switch (status.getVisibility()) { + case PUBLIC: + case UNLISTED: { + final String textId = + getString(status.isPinned() ? R.string.unpin_action : R.string.pin_action); + menu.add(0, R.id.pin, 1, textId); + break; + } + case PRIVATE: { + boolean reblogged = status.getReblogged(); + if (status.getReblog() != null) reblogged = status.getReblog().getReblogged(); + menu.findItem(R.id.status_reblog_private).setVisible(!reblogged); + menu.findItem(R.id.status_unreblog_private).setVisible(reblogged); + break; + } + } + } + + Menu menu = popup.getMenu(); + MenuItem openAsItem = menu.findItem(R.id.status_open_as); + switch (accounts.size()) { + case 0: + case 1: + openAsItem.setVisible(false); + break; + case 2: + for (AccountEntity account : accounts) { + if (account != activeAccount) { + openAsTitle = String.format(getString(R.string.action_open_as), account.getFullName()); + break; + } + } + break; + default: + openAsTitle = String.format(getString(R.string.action_open_as), "…"); + break; + } + openAsItem.setTitle(openAsTitle); + + // maybe not a best check + if(status.getPleroma() != null) { + boolean showMute = true; // predict state + + if(status.isThreadMuted() == true) { + showMute = false; + } + + // show mutes only for Pleroma because Mastodon don't handle them in sane way + // e.g. why you can only mute threads where you were participated? + menu.findItem(R.id.status_mute_conversation).setVisible(showMute); + menu.findItem(R.id.status_unmute_conversation).setVisible(!showMute); + } + + popup.setOnMenuItemClickListener(item -> { + switch (item.getItemId()) { + case R.id.status_share_content: { + Status statusToShare = status; + if (statusToShare.getReblog() != null) + statusToShare = statusToShare.getReblog(); + + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + + String stringToShare = statusToShare.getAccount().getUsername() + + " - " + + statusToShare.getContent().toString(); + sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to))); + return true; + } + case R.id.status_share_link: { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to))); + return true; + } + case R.id.status_copy_link: { + ClipboardManager clipboard = (ClipboardManager) + getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(null, statusUrl); + clipboard.setPrimaryClip(clip); + return true; + } + case R.id.status_open_in_web: { + LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), getContext()); + return true; + } + case R.id.status_open_as: { + showOpenAsDialog(statusUrl, item.getTitle()); + return true; + } + case R.id.status_download_media: { + requestDownloadAllMedia(status); + return true; + } + case R.id.status_mute: { + onMute(accountId, accountUsername); + return true; + } + case R.id.status_block: { + onBlock(accountId, accountUsername); + return true; + } + case R.id.status_report: { + openReportPage(accountId, accountUsername, id); + return true; + } + case R.id.status_mute_conversation: { + timelineCases.muteConversation(status, true); + return true; + } + case R.id.status_unmute_conversation: { + timelineCases.muteConversation(status, false); + return true; + } + case R.id.status_unreblog_private: { + onReblog(false, position); + return true; + } + case R.id.status_reblog_private: { + onReblog(true, position); + return true; + } + case R.id.status_delete: { + showConfirmDeleteDialog(id, position); + return true; + } + case R.id.pin: { + timelineCases.pin(status, !status.isPinned()); + return true; + } + } + return false; + }); + popup.show(); + } + + private void onMute(String accountId, String accountUsername) { + MuteAccountDialog.showMuteAccountDialog( + this.getActivity(), + accountUsername, + (notifications, duration) -> { + timelineCases.mute(accountId, notifications, duration); + return Unit.INSTANCE; + } + ); + } + + private void onBlock(String accountId, String accountUsername) { + new AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton(android.R.string.ok, (__, ___) -> timelineCases.block(accountId)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private static boolean accountIsInMentions(AccountEntity account, Status.Mention[] mentions) { + if (account == null) { + return false; + } + + for (Status.Mention mention : mentions) { + if (account.getUsername().equals(mention.getUsername())) { + Uri uri = Uri.parse(mention.getUrl()); + if (uri != null && account.getDomain().equals(uri.getHost())) { + return true; + } + } + } + return false; + } + + protected void viewMedia(int urlIndex, Status status, @Nullable View view) { + final Status actionable = status.getActionableStatus(); + final Attachment active = actionable.getAttachments().get(urlIndex); + Attachment.Type type = active.getType(); + switch (type) { + case GIFV: + case VIDEO: + case IMAGE: + case AUDIO: { + final List attachments = AttachmentViewData.list(actionable); + final Intent intent = ViewMediaActivity.newIntent(getContext(), attachments, + urlIndex); + if (view != null) { + String url = active.getUrl(); + ViewCompat.setTransitionName(view, url); + ActivityOptionsCompat options = + ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), + view, url); + startActivity(intent, options.toBundle()); + } else { + startActivity(intent); + } + break; + } + default: + case UNKNOWN: { + LinkHelper.openLink(active.getUrl(), getContext()); + break; + } + } + } + + protected void viewTag(String tag) { + Intent intent = new Intent(getContext(), ViewTagActivity.class); + intent.putExtra("hashtag", tag); + startActivity(intent); + } + + protected void openReportPage(String accountId, String accountUsername, String statusId) { + startActivity(ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId)); + } + + protected void showConfirmDeleteDialog(final String id, final int position) { + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.dialog_delete_toot_warning) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + timelineCases.delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + deletedStatus -> { + }, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show(); + }); + removeItem(position); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void showConfirmEditDialog(final String id, final int position, final Status status) { + if (getActivity() == null) { + return; + } + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.dialog_redraft_toot_warning) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + timelineCases.delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe(deletedStatus -> { + removeItem(position); + + if (deletedStatus.isEmpty()) { + deletedStatus = status.toDeletedStatus(); + } + ComposeOptions composeOptions = new ComposeOptions(); + composeOptions.setTootText(deletedStatus.getText()); + composeOptions.setInReplyToId(deletedStatus.getInReplyToId()); + composeOptions.setVisibility(deletedStatus.getVisibility()); + composeOptions.setContentWarning(deletedStatus.getSpoilerText()); + composeOptions.setMediaAttachments(deletedStatus.getAttachments()); + composeOptions.setSensitive(deletedStatus.getSensitive()); + composeOptions.setModifiedInitialState(true); + if (deletedStatus.getPoll() != null) { + composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt())); + } + + Intent intent = ComposeActivity + .startIntent(getContext(), composeOptions); + startActivity(intent); + }, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show(); + }); + + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void openAsAccount(String statusUrl, AccountEntity account) { + accountManager.setActiveAccount(account); + Intent intent = new Intent(getContext(), MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra(MainActivity.STATUS_URL, statusUrl); + startActivity(intent); + ((BaseActivity) getActivity()).finishWithoutSlideOutAnimation(); + } + + private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) { + BaseActivity activity = (BaseActivity) getActivity(); + activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account)); + } + + private void downloadAllMedia(Status status) { + Toast.makeText(getContext(), R.string.downloading_media, Toast.LENGTH_SHORT).show(); + for (Attachment attachment : status.getAttachments()) { + String url = attachment.getUrl(); + Uri uri = Uri.parse(url); + String filename = uri.getLastPathSegment(); + + DownloadManager downloadManager = (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(uri); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); + downloadManager.enqueue(request); + } + } + + private void requestDownloadAllMedia(Status status) { + String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; + ((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + downloadAllMedia(status); + } else { + Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show(); + } + }); + } + + public boolean isFilteringMuted() { + return filterMuted; + } + + public void updateMuteFilter(@NonNull SharedPreferences pref, boolean reload) { + filterMuted = pref.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); + + if(reload) { + refreshAfterApplyingFilters(); + } + } + + public void reloadFilters(SharedPreferences pref, boolean forceRefresh) { + if(pref != null) { + updateMuteFilter(pref, false); // will be reloaded later + } + + if (filters != null && !forceRefresh) { + applyFilters(forceRefresh); + return; + } + + mastodonApi.getFilters().enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + filters = response.body(); + if (response.isSuccessful() && filters != null) { + applyFilters(forceRefresh); + } else { + Log.e(TAG, "Error getting filters from server"); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + Log.e(TAG, "Error getting filters from server", t); + } + }); + } + + protected boolean filterIsRelevant(@NonNull Filter filter) { + // Called when building local filter expression + // Override to select relevant filters for your fragment + return false; + } + + protected void refreshAfterApplyingFilters() { + // Called after filters are updated + // Override to refresh your fragment + } + + @VisibleForTesting + public boolean shouldFilterStatus(Status status) { + if (filterMuted && status.getMuted()) { + return true; + } + + if (filterRemoveRegex && status.getPoll() != null) { + for (PollOption option : status.getPoll().getOptions()) { + if (filterRemoveRegexMatcher.reset(option.getTitle()).find()) { + return true; + } + } + } + + return (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find() + || (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find()))); + } + + public void applyFilters(boolean refresh) { + List tokens = new ArrayList<>(); + for (Filter filter : filters) { + if (filterIsRelevant(filter)) { + tokens.add(filterToRegexToken(filter)); + } + } + filterRemoveRegex = !tokens.isEmpty(); + if (filterRemoveRegex) { + filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher(""); + } + if (refresh) { + refreshAfterApplyingFilters(); + } + } + + private static String filterToRegexToken(Filter filter) { + String phrase = filter.getPhrase(); + String quotedPhrase = Pattern.quote(phrase); + return (filter.getWholeWord() && alphanumeric.reset(phrase).matches()) ? // "whole word" should only apply to alphanumeric filters, #1543 + String.format("(^|\\W)%s($|\\W)", quotedPhrase) : + quotedPhrase; + } + + public static void flushFilters() { + filters = null; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java new file mode 100644 index 0000000..1349a59 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java @@ -0,0 +1,53 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.fragment; + +import android.app.Dialog; +import android.app.TimePickerDialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + +import com.keylesspalace.tusky.components.compose.ComposeActivity; + +import java.util.Calendar; +import java.util.TimeZone; + +public class TimePickerFragment extends DialogFragment { + + public static final String PICKER_TIME_HOUR = "picker_time_hour"; + public static final String PICKER_TIME_MINUTE = "picker_time_minute"; + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); + if (args != null) { + calendar.set(Calendar.HOUR_OF_DAY, args.getInt(PICKER_TIME_HOUR)); + calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE)); + } + + return new TimePickerDialog(getContext(), + android.R.style.Theme_DeviceDefault_Dialog, + (ComposeActivity) getActivity(), + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + true); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java new file mode 100644 index 0000000..250ccb5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -0,0 +1,1648 @@ +/* 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.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; +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.*; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.entity.*; +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.concurrent.TimeUnit; +import java.util.Objects; + +import javax.inject.Inject; + +import at.connyduck.sparkbutton.helpers.Utils; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; +import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function1; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + +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 = Objects.requireNonNull(getArguments()); + 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.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, 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 + 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(preferences,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); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + + /* 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. */ + 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 + activity.onActionButtonHidden(); + } 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 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 MuteConversationEvent) { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + handleMuteStatusEvent((MuteConversationEvent)event); + } + } else if (event instanceof MuteEvent) { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + handleMuteEvent((MuteEvent)event); + } + } 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()); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent)event); + } + }); + 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(); + } + + @Override + public void onMute(int position, boolean isMuted) { + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder((StatusViewData.Concrete)statuses.getPairedItem(position)) + .setMuted(isMuted) + .createStatusViewData(); + statuses.setPairedItem(position, statusViewData); + updateAdapter(); + } + + private void setMutedStatusForStatus(int position, Status status, boolean muted, boolean threadMuted) { + status.setThreadMuted(threadMuted); + + StatusViewData.Builder statusViewData = new StatusViewData.Builder((StatusViewData.Concrete)statuses.getPairedItem(position)); + statusViewData.setMuted(muted); + statusViewData.setThreadMuted(threadMuted); + + statuses.setPairedItem(position, statusViewData.createStatusViewData()); + } + + 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 onViewReplyTo(int position) { + Status status = statuses.get(position).asRightOrNull(); + if (status == null) return; + + String replyToId = status.getReblog() == null ? status.getInReplyToId() : status.getReblog().getInReplyToId(); + if (replyToId == null) return; + super.onShowReplyTo(replyToId); + } + + @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 PrefKeys.HIDE_MUTED_USERS: { + updateMuteFilter(sharedPreferences, true); + break; + } + case Filter.HOME: + case Filter.NOTIFICATIONS: + case Filter.THREAD: + case Filter.PUBLIC: + case Filter.ACCOUNT: { + if (filterContextMatchesKind(kind, Collections.singletonList(key))) { + reloadFilters(sharedPreferences, 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 removeAllByConversationId(int conversationId) { + // using iterator to safely remove items while iterating + Iterator> iterator = statuses.iterator(); + while (iterator.hasNext()) { + Status status = iterator.next().asRightOrNull(); + if (status != null && + (status.getConversationId() == conversationId) || status.getActionableStatus().getConversationId() == conversationId) { + iterator.remove(); + } + } + 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 Call> 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(new Exception(err), fetchEnd, pos) + ); + } else { + Callback> callback = new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + if (response.isSuccessful()) { + @Nullable + String newNextId = extractNextId(response); + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId; + } + onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); + } else { + onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + onFetchTimelineFailure((Exception) t, fetchEnd, pos); + } + }; + + Call> listCall = getFetchCallByTimelineType(maxId, sinceId); + callList.add(listCall); + listCall.enqueue(callback); + } + } + + @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(Exception exception, 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 (exception 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: " + exception.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 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 void handleMuteStatusEvent(MuteConversationEvent event) { + int pos = findStatusOrReblogPositionById(event.getStatusId()); + + if (pos < 0) + return; + + Status eventStatus = statuses.get(pos).asRight(); + int conversationId = eventStatus.getConversationId(); + + if(conversationId == -1) { // invalid conversation ID + if(isFilteringMuted()) { + statuses.remove(pos); + } else { + setMutedStatusForStatus(pos, eventStatus, event.getMute(), event.getMute()); + } + updateAdapter(); + } else { + //noinspection ConstantConditions + if(isFilteringMuted()) { + removeAllByConversationId(conversationId); + } else { + for (int i = 0; i < statuses.size(); i++) { + Status status = statuses.get(i).asRightOrNull(); + if (status != null && status.getConversationId() == conversationId) { + setMutedStatusForStatus(i, status, event.getMute(), event.getMute()); + } + } + updateAdapter(); + } + } + } + + private void handleMuteEvent(MuteEvent event) { + String id = event.getAccountId(); + boolean muting = event.getMute(); + + if(isFilteringMuted() && muting) { + removeAllByAccountId(id); + } else { + for (int i = 0; i < statuses.size(); i++) { + Status status = statuses.get(i).asRightOrNull(); + if (status != null + && status.getAccount().getId().equals(id) + && !status.isThreadMuted()) { + setMutedStatusForStatus(i, status, muting, false); + } + } + updateAdapter(); + } + } + + 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; + } + }; + + @Override + public void onResume() { + super.onResume(); + 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; + } + + private void setEmojiReactionForStatus(int position, Status newStatus) { + StatusViewData newViewData = ViewDataUtils.statusToViewData(newStatus, false, false); + statuses.setPairedItem(position, newViewData); + updateAdapter(); + } + + private void setEmojiReactForStatus(int position, Status status, Status newStatus) { + Pair actual = + findStatusAndPosition(position, status); + if (actual == null) return; + + setEmojiReactionForStatus(actual.second, newStatus); + } + + public void handleEmojiReactEvent(EmojiReactEvent event) { + int pos = findStatusOrReblogPositionById(event.getNewStatus().getActionableId()); + if (pos < 0) return; + Status status = statuses.get(pos).asRight(); + setEmojiReactForStatus(pos, status, event.getNewStatus()); + } + + @Override + public void onEmojiReact(final boolean react, final String emoji, final String statusId) { + int position = findStatusOrReblogPositionById(statusId); + if (position < 0) return; + + timelineCases.react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactionForStatus(position, newStatus), + (t) -> Log.d(TAG, + "Failed to react with " + emoji + " on status: " + statusId, t) + ); + + } + + + @Override + public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) { + super.emojiReactMenu(statusId, emoji, view, this); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt new file mode 100644 index 0000000..f7d8538 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -0,0 +1,288 @@ +/* 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.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.view.* +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.github.piasy.biv.BigImageViewer +import com.github.piasy.biv.loader.ImageLoader +import com.github.piasy.biv.view.GlideImageViewFactory +import com.keylesspalace.tusky.R +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 java.io.File +import java.lang.Exception +import kotlin.math.abs +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.github.piasy.biv.view.BigImageView + + +class ViewImageFragment : ViewMediaFragment() { + interface PhotoActionsListener { + fun onBringUp() + fun onDismiss() + fun onPhotoTap() + } + + private lateinit var photoActionsListener: PhotoActionsListener + private lateinit var toolbar: View + private var shouldStartTransition = false + + // Volatile: Image requests happen on background thread and we want to see updates to it + // immediately on another thread. Atomic is an overkill for such thing. + @Volatile + private var startedTransition = false + + private var uri = Uri.EMPTY + private var previewUri = Uri.EMPTY + private var showingPreview = false + + override fun onAttach(context: Context) { + super.onAttach(context) + photoActionsListener = context as PhotoActionsListener + } + + override fun setupMediaView(url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean) { + photoView.transitionName = url + mediaDescription.text = description + captionSheet.visible(showingDescription) + + startedTransition = false + uri = Uri.parse(url) + if(previewUrl != null && !previewUrl.equals(url)) { + previewUri = Uri.parse(previewUrl) + } + loadImageFromNetwork() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + toolbar = activity!!.toolbar + return inflater.inflate(R.layout.fragment_view_image, container, false) + } + + private val imageOnTouchListener = object : View.OnTouchListener { + private var lastY = 0.0f + private var swipeStartedWithOneFinger = false + + override fun onTouch(v: View, event: MotionEvent): Boolean { + // This part is for scaling/translating on vertical move. + // We use raw coordinates to get the correct ones during scaling + + if(event.pointerCount != 1) { + onGestureEnd() + swipeStartedWithOneFinger = false + return false + } + + when(event.action) { + MotionEvent.ACTION_DOWN -> { + swipeStartedWithOneFinger = true + lastY = event.rawY + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + onGestureEnd() + swipeStartedWithOneFinger = false + } + MotionEvent.ACTION_MOVE -> { + if(swipeStartedWithOneFinger && + (photoView.ssiv == null || photoView.ssiv.scale <= photoView.ssiv.minScale)) { + 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 + lastY = event.rawY + } + } + } + } + + return false + } + } + + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + photoView.setImageLoaderCallback(imageLoaderCallback) + photoView.setImageViewFactory(GlideImageViewFactory()) + + val arguments = this.requireArguments() + val attachment = arguments.getParcelable(ARG_ATTACHMENT) + this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION) + val url: String? + var description: String? = null + + if (attachment != null) { + url = attachment.url + description = attachment.description + } else { + url = arguments.getString(ARG_AVATAR_URL) + if (url == null) { + throw IllegalArgumentException("attachment or avatar url has to be set") + } + } + + finalizeViewSetup(url, attachment?.previewUrl, description) + } + + private fun onGestureEnd() { + if (abs(photoView.translationY) > 180) { + photoActionsListener.onDismiss() + } else { + photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + } + } + + private fun onMediaTap() { + photoActionsListener.onPhotoTap() + } + + override fun onToolbarVisibilityChange(visible: Boolean) { + if (photoView == null || !userVisibleHint || captionSheet == null) { + return + } + isDescriptionVisible = showingDescription && visible + val alpha = if (isDescriptionVisible) 1.0f else 0.0f + captionSheet.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + captionSheet?.visible(isDescriptionVisible) + animation.removeListener(this) + } + }) + .start() + } + + override fun onDestroyView() { + super.onDestroyView() + photoView.ssiv?.recycle() + } + + private inner class DummyCacheTarget(val ctx: Context, val requestPreview : Boolean) : CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) {} + override fun onLoadFailed(errorDrawable: Drawable?) { + if(requestPreview) { + // no preview, no full image in cache, load full image + // forget about fancy transition + showingPreview = false + photoView.showImage(uri) + photoActionsListener.onBringUp() + } else { + // let's start downloading full image that we supposedly don't have + BigImageViewer.prefetch(uri) + + // meanwhile poke cache about preview image + Glide.with(ctx).asFile() + .load(previewUri) + .dontAnimate() + .onlyRetrieveFromCache(true) + .into(DummyCacheTarget(ctx, true)) + } + } + + override fun onResourceReady(resource: File, transition: Transition?) { + showingPreview = requestPreview + if(requestPreview) { + // have preview cached but not full image + photoView.showImage(previewUri, uri, true) + } else { + photoView.showImage(uri) + } + photoActionsListener.onBringUp() + } + } + + private fun loadImageFromNetwork() { + if(previewUri != Uri.EMPTY) { + // check if we have full image in the cache, if yes, use it + // if not, look for preview in cache and use it if available + // if not, load full image anyway + Glide.with(this).asFile() + .load(uri) + .onlyRetrieveFromCache(true) + .dontAnimate() + .into(DummyCacheTarget(context!!, false)) + } else { + // no need in cache lookup, just load full image + showingPreview = false + photoView.showImage(uri) + photoActionsListener.onBringUp() + } + } + + override fun onTransitionEnd() { + // if we had preview, load full image, as transition has ended + if (showingPreview) { + showingPreview = false + photoView.showImage(uri) + } + } + + private val imageLoaderCallback = object : ImageLoader.Callback { + override fun onSuccess(image: File?) { + if(!showingPreview) { + progressBar?.hide() + + photoView.setInitScaleType(BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE) + photoView.ssiv?.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF + photoView.mainView?.setOnTouchListener(imageOnTouchListener) + } + } + + override fun onFail(error: Exception?) { + progressBar?.hide() + } + + override fun onCacheHit(imageType: Int, image: File?) { + } + + override fun onStart() { + } + + override fun onCacheMiss(imageType: Int, image: File?) { + // this callback is useless because it's called after + // image is downloaded or pulled from cache + // so in case of cache miss, onStart is used + } + + override fun onFinish() {} + override fun onProgress(progress: Int) { + // TODO: make use of it :) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt new file mode 100644 index 0000000..9b51f28 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -0,0 +1,95 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.fragment + +import android.os.Bundle +import android.text.TextUtils +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.entity.Attachment + +abstract class ViewMediaFragment : BaseFragment() { + private var toolbarVisibiltyDisposable: Function0? = null + + abstract fun setupMediaView( + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean + ) + + abstract fun onToolbarVisibilityChange(visible: Boolean) + + protected var showingDescription = false + protected var isDescriptionVisible = false + + companion object { + @JvmStatic + protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition" + + @JvmStatic + protected val ARG_ATTACHMENT = "attach" + @JvmStatic + protected val ARG_AVATAR_URL = "avatarUrl" + + @JvmStatic + fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment { + val arguments = Bundle(2) + arguments.putParcelable(ARG_ATTACHMENT, attachment) + arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition) + + val fragment = when (attachment.type) { + Attachment.Type.IMAGE -> ViewImageFragment() + Attachment.Type.VIDEO, + Attachment.Type.GIFV, + Attachment.Type.AUDIO -> ViewVideoFragment() + else -> ViewImageFragment() // it probably won't show anything, but its better than crashing + } + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newAvatarInstance(avatarUrl: String): ViewMediaFragment { + val arguments = Bundle(2) + val fragment = ViewImageFragment() + arguments.putString(ARG_AVATAR_URL, avatarUrl) + arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true) + + fragment.arguments = arguments + return fragment + } + } + + abstract fun onTransitionEnd() + + protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) { + val mediaActivity = activity as ViewMediaActivity + + showingDescription = !TextUtils.isEmpty(description) + isDescriptionVisible = showingDescription + setupMediaView(url, previewUrl, description, showingDescription && mediaActivity.isToolbarVisible) + + toolbarVisibiltyDisposable = (activity as ViewMediaActivity) + .addToolbarVisibilityListener { isVisible -> + onToolbarVisibilityChange(isVisible) + } + } + + override fun onDestroyView() { + toolbarVisibiltyDisposable?.invoke() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java new file mode 100644 index 0000000..0dcc931 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -0,0 +1,821 @@ +/* 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.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; +import androidx.core.util.Pair; +import androidx.lifecycle.Lifecycle; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.snackbar.Snackbar; +import com.keylesspalace.tusky.AccountListActivity; +import com.keylesspalace.tusky.BaseActivity; +import com.keylesspalace.tusky.BuildConfig; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.ViewThreadActivity; +import com.keylesspalace.tusky.adapter.ThreadAdapter; +import com.keylesspalace.tusky.appstore.*; +import com.keylesspalace.tusky.di.Injectable; +import com.keylesspalace.tusky.entity.*; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.network.MastodonApi; +import com.keylesspalace.tusky.settings.PrefKeys; +import com.keylesspalace.tusky.util.CardViewMode; +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.view.ConversationLineItemDecoration; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import javax.inject.Inject; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + +public final class ViewThreadFragment extends SFragment implements + SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable { + private static final String TAG = "ViewThreadFragment"; + + @Inject + public MastodonApi mastodonApi; + @Inject + public EventHub eventHub; + + private SwipeRefreshLayout swipeRefreshLayout; + private RecyclerView recyclerView; + private ThreadAdapter adapter; + private String thisThreadsStatusId; + private boolean alwaysShowSensitiveMedia; + private boolean alwaysOpenSpoiler; + + private int statusIndex = 0; + + private final PairedList statuses = + new PairedList<>(new Function() { + @Override + public StatusViewData.Concrete apply(Status input) { + return ViewDataUtils.statusToViewData( + input, + alwaysShowSensitiveMedia, + alwaysOpenSpoiler + ); + } + }); + + public static ViewThreadFragment newInstance(String id) { + Bundle arguments = new Bundle(1); + ViewThreadFragment fragment = new ViewThreadFragment(); + arguments.putString("id", id); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + thisThreadsStatusId = getArguments().getString("id"); + 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.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + ); + adapter = new ThreadAdapter(statusDisplayOptions, this); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); + + Context context = getContext(); + swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); + swipeRefreshLayout.setOnRefreshListener(this); + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); + + recyclerView = rootView.findViewById(R.id.recyclerView); + recyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAccessibilityDelegateCompat( + new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); + DividerItemDecoration divider = new DividerItemDecoration( + context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(divider); + + recyclerView.addItemDecoration(new ConversationLineItemDecoration(context)); + alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); + alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); + reloadFilters(PreferenceManager.getDefaultSharedPreferences(context), false); + + recyclerView.setAdapter(adapter); + + statuses.clear(); + + ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); + + return rootView; + } + + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + onRefresh(); + + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe(event -> { + if (event instanceof FavoriteEvent) { + handleFavEvent((FavoriteEvent) event); + } else if (event instanceof ReblogEvent) { + handleReblogEvent((ReblogEvent) event); + } else if (event instanceof BookmarkEvent) { + handleBookmarkEvent((BookmarkEvent) event); + } else if (event instanceof BlockEvent) { + removeAllByAccountId(((BlockEvent) event).getAccountId()); + } else if (event instanceof MuteEvent) { + handleMuteEvent((MuteEvent) event); + } else if (event instanceof StatusComposedEvent) { + handleStatusComposedEvent((StatusComposedEvent) event); + } else if (event instanceof StatusDeletedEvent) { + handleStatusDeletedEvent((StatusDeletedEvent) event); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent)event); + } + }); + + if(thisThreadsStatusPosition != -1) { + recyclerView.scrollToPosition(thisThreadsStatusPosition); + } + } + + public void onRevealPressed() { + boolean allExpanded = allExpanded(); + for (int i = 0; i < statuses.size(); i++) { + StatusViewData.Concrete newViewData = + new StatusViewData.Concrete.Builder(statuses.getPairedItem(i)) + .setIsExpanded(!allExpanded) + .createStatusViewData(); + statuses.setPairedItem(i, newViewData); + } + updateAdapter(); + updateRevealIcon(); + } + + private boolean allExpanded() { + boolean allExpanded = true; + for (int i = 0; i < statuses.size(); i++) { + if (!statuses.getPairedItem(i).isExpanded()) { + allExpanded = false; + break; + } + } + return allExpanded; + } + + @Override + public void onRefresh() { + sendStatusRequest(thisThreadsStatusId); + sendThreadRequest(thisThreadsStatusId); + } + + @Override + public void onReply(int position) { + super.reply(statuses.get(position)); + } + + @Override + public void onReblog(final boolean reblog, final int position) { + final Status status = statuses.get(position); + + timelineCases.reblog(statuses.get(position), reblog) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, + "Failed to reblog status: " + status.getId(), t) + ); + } + + @Override + public void onFavourite(final boolean favourite, final int position) { + final Status status = statuses.get(position); + + timelineCases.favourite(statuses.get(position), favourite) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, + "Failed to favourite status: " + status.getId(), t) + ); + } + + @Override + public void onBookmark(final boolean bookmark, final int position) { + final Status status = statuses.get(position); + + timelineCases.bookmark(statuses.get(position), bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, + "Failed to bookmark status: " + status.getId(), t) + ); + } + + private void updateStatus(int position, Status status) { + if (position >= 0 && position < statuses.size()) { + + Status actionableStatus = status.getActionableStatus(); + + StatusViewData.Concrete viewData = new StatusViewData.Builder(statuses.getPairedItem(position)) + .setReblogged(actionableStatus.getReblogged()) + .setReblogsCount(actionableStatus.getReblogsCount()) + .setFavourited(actionableStatus.getFavourited()) + .setBookmarked(actionableStatus.getBookmarked()) + .setFavouritesCount(actionableStatus.getFavouritesCount()) + .createStatusViewData(); + statuses.setPairedItem(position, viewData); + + adapter.setItem(position, viewData, true); + + } + } + + @Override + public void onMore(@NonNull View view, int position) { + super.more(statuses.get(position), view, position); + } + + @Override + public void onViewMedia(int position, int attachmentIndex, @NonNull View view) { + Status status = statuses.get(position); + super.viewMedia(attachmentIndex, status, view); + } + + @Override + public void onViewThread(int position) { + Status status = statuses.get(position); + if (thisThreadsStatusId.equals(status.getId())) { + // If already viewing this thread, don't reopen it. + return; + } + super.viewThread(status); + } + + @Override + public void onViewReplyTo(int position) { + Status status = statuses.get(position); + if (thisThreadsStatusId.equals(status.getInReplyToId())) return; + super.onShowReplyTo(status.getInReplyToId()); + } + + @Override + public void onOpenReblog(int position) { + // there should be no reblogs in the thread but let's implement it to be sure + super.openReblog(statuses.get(position)); + } + + @Override + public void onExpandedChange(boolean expanded, int position) { + StatusViewData.Concrete newViewData = + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setIsExpanded(expanded) + .createStatusViewData(); + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); + updateRevealIcon(); + } + + @Override + public void onContentHiddenChange(boolean isShowing, int position) { + StatusViewData.Concrete newViewData = + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); + } + + @Override + public void onLoadMore(int position) { + + } + + @Override + public void onShowReblogs(int position) { + String statusId = statuses.get(position).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).getId(); + Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId); + ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); + } + + @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.Concrete status = statuses.getPairedItem(position); + if (status == null) { + // 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 null instead at position: %d of %d", + position, + statuses.size() - 1 + )); + return; + } + + StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status) + .setCollapsed(isCollapsed) + .createStatusViewData(); + statuses.setPairedItem(position, updatedStatus); + recyclerView.post(() -> adapter.setItem(position, updatedStatus, true)); + } + + @Override + public void onViewTag(String tag) { + super.viewTag(tag); + } + + @Override + public void onViewAccount(String id) { + super.viewAccount(id); + } + + @Override + public void removeItem(int position) { + if (position == statusIndex) { + //the status got removed, close the activity + getActivity().finish(); + } + statuses.remove(position); + updateAdapter(); + } + + public void onVoteInPoll(int position, @NonNull List choices) { + final Status status = statuses.get(position).getActionableStatus(); + + setVoteForPoll(position, status.getPoll().votedCopy(choices)); + + timelineCases.voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newPoll) -> setVoteForPoll(position, newPoll), + (t) -> Log.d(TAG, + "Failed to vote in poll: " + status.getId(), t) + ); + + } + + private void setVoteForPoll(int position, Poll newPoll) { + + StatusViewData.Concrete viewData = statuses.getPairedItem(position); + + StatusViewData.Concrete newViewData = new StatusViewData.Builder(viewData) + .setPoll(newPoll) + .createStatusViewData(); + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); + } + + private void updateAdapter() { + adapter.setStatuses(statuses.getPairedCopy()); + } + + private void removeAllByAccountId(String accountId) { + Status status = null; + if (!statuses.isEmpty()) { + status = statuses.get(statusIndex); + } + // using iterator to safely remove items while iterating + Iterator iterator = statuses.iterator(); + while (iterator.hasNext()) { + Status s = iterator.next(); + if (s.getAccount().getId().equals(accountId) || s.getActionableStatus().getAccount().getId().equals(accountId)) { + iterator.remove(); + } + } + statusIndex = statuses.indexOf(status); + if (statusIndex == -1) { + //the status got removed, close the activity + getActivity().finish(); + return; + } + adapter.setDetailedStatusPosition(statusIndex); + updateAdapter(); + } + + private void sendStatusRequest(final String id) { + Call call = mastodonApi.status(id); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + int position = setStatus(response.body()); + recyclerView.scrollToPosition(position); + } else { + onThreadRequestFailure(id); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + onThreadRequestFailure(id); + } + }); + callList.add(call); + } + + private void sendThreadRequest(final String id) { + Call call = mastodonApi.statusContext(id); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + StatusContext context = response.body(); + if (response.isSuccessful() && context != null) { + swipeRefreshLayout.setRefreshing(false); + setContext(context.getAncestors(), context.getDescendants()); + } else { + onThreadRequestFailure(id); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + onThreadRequestFailure(id); + } + }); + callList.add(call); + } + + private void onThreadRequestFailure(final String id) { + View view = getView(); + swipeRefreshLayout.setRefreshing(false); + if (view != null) { + Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry, v -> { + sendThreadRequest(id); + sendStatusRequest(id); + }) + .show(); + } else { + Log.e(TAG, "Couldn't display thread fetch error message"); + } + } + + private int setStatus(Status status) { + if (statuses.size() > 0 + && statusIndex < statuses.size() + && statuses.get(statusIndex).equals(status)) { + // Do not add this status on refresh, it's already in there. + statuses.set(statusIndex, status); + return statusIndex; + } + int i = statusIndex; + statuses.add(i, status); + adapter.setDetailedStatusPosition(i); + adapter.addItem(i, statuses.getPairedItem(i)); + updateRevealIcon(); + return i; + } + + private void setContext(List unfilteredAncestors, List unfilteredDescendants) { + Status mainStatus = null; + + // In case of refresh, remove old ancestors and descendants first. We'll remove all blindly, + // as we have no guarantee on their order to be the same as before + int oldSize = statuses.size(); + if (oldSize > 1) { + mainStatus = statuses.get(statusIndex); + statuses.clear(); + adapter.clearItems(); + } + + ArrayList ancestors = new ArrayList<>(); + for (Status status : unfilteredAncestors) + if (!shouldFilterStatus(status)) + ancestors.add(status); + + // Insert newly fetched ancestors + statusIndex = ancestors.size(); + adapter.setDetailedStatusPosition(statusIndex); + statuses.addAll(0, ancestors); + List ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex); + if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { + String error = String.format(Locale.getDefault(), + "Incorrectly got statusViewData sublist." + + " ancestors.size == %d ancestorsViewDatas.size == %d," + + " statuses.size == %d", + ancestors.size(), ancestorsViewDatas.size(), statuses.size()); + throw new AssertionError(error); + } + adapter.addAll(0, ancestorsViewDatas); + + if (mainStatus != null) { + // In case we needed to delete everything (which is way easier than deleting + // everything except one), re-insert the remaining status here. + // Not filtering the main status, since the user explicitly chose to be here + statuses.add(statusIndex, mainStatus); + StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex); + + adapter.addItem(statusIndex, viewData); + } + + ArrayList descendants = new ArrayList<>(); + for (Status status : unfilteredDescendants) + if (!shouldFilterStatus(status)) + descendants.add(status); + + // Insert newly fetched descendants + statuses.addAll(descendants); + List descendantsViewData; + descendantsViewData = statuses.getPairedCopy() + .subList(statuses.size() - descendants.size(), statuses.size()); + if (BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) { + String error = String.format(Locale.getDefault(), + "Incorrectly got statusViewData sublist." + + " descendants.size == %d descendantsViewData.size == %d," + + " statuses.size == %d", + descendants.size(), descendantsViewData.size(), statuses.size()); + throw new AssertionError(error); + } + adapter.addAll(descendantsViewData); + updateRevealIcon(); + } + + private void setMutedStatusForStatus(int position, Status status, boolean muted) { + StatusViewData.Builder statusViewData = new StatusViewData.Builder(statuses.getPairedItem(position)); + statusViewData.setMuted(muted); + + statuses.setPairedItem(position, statusViewData.createStatusViewData()); + } + + private void handleMuteEvent(MuteEvent event) { + String id = event.getAccountId(); + boolean muting = event.getMute(); + + if(isFilteringMuted()) { + removeAllByAccountId(id); + } else { + for (int i = 0; i < statuses.size(); i++) { + Status status = statuses.get(i); + if (status != null + && status.getAccount().getId().equals(id) + && !status.isThreadMuted()) { + setMutedStatusForStatus(i, status, muting); + } + } + updateAdapter(); + } + } + + + private void handleFavEvent(FavoriteEvent event) { + Pair posAndStatus = findStatusAndPos(event.getStatusId()); + if (posAndStatus == null) return; + + boolean favourite = event.getFavourite(); + posAndStatus.second.setFavourited(favourite); + + if (posAndStatus.second.getReblog() != null) { + posAndStatus.second.getReblog().setFavourited(favourite); + } + + StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setFavourited(favourite); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(posAndStatus.first, newViewData); + adapter.setItem(posAndStatus.first, newViewData, true); + } + + private void handleReblogEvent(ReblogEvent event) { + Pair posAndStatus = findStatusAndPos(event.getStatusId()); + if (posAndStatus == null) return; + + boolean reblog = event.getReblog(); + posAndStatus.second.setReblogged(reblog); + + if (posAndStatus.second.getReblog() != null) { + posAndStatus.second.getReblog().setReblogged(reblog); + } + + StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setReblogged(reblog); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(posAndStatus.first, newViewData); + adapter.setItem(posAndStatus.first, newViewData, true); + } + + private void handleBookmarkEvent(BookmarkEvent event) { + Pair posAndStatus = findStatusAndPos(event.getStatusId()); + if (posAndStatus == null) return; + + boolean bookmark = event.getBookmark(); + posAndStatus.second.setBookmarked(bookmark); + + if (posAndStatus.second.getReblog() != null) { + posAndStatus.second.getReblog().setBookmarked(bookmark); + } + + StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setBookmarked(bookmark); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(posAndStatus.first, newViewData); + adapter.setItem(posAndStatus.first, newViewData, true); + } + + private void handleStatusComposedEvent(StatusComposedEvent event) { + Status eventStatus = event.getStatus(); + if (eventStatus.getInReplyToId() == null) return; + + if (eventStatus.getInReplyToId().equals(thisThreadsStatusId)) { + insertStatus(eventStatus, statuses.size()); + } else { + // If new status is a reply to some status in the thread, insert new status after it + // We only check statuses below main status, ones on top don't belong to this thread + for (int i = statusIndex; i < statuses.size(); i++) { + Status status = statuses.get(i); + if (eventStatus.getInReplyToId().equals(status.getId())) { + insertStatus(eventStatus, i + 1); + break; + } + } + } + } + + private int thisThreadsStatusPosition = -1; + private void insertStatus(Status status, int at) { + statuses.add(at, status); + adapter.addItem(at, statuses.getPairedItem(at)); + if(status.getId().equals(thisThreadsStatusId)) { + thisThreadsStatusPosition = at; + } + } + + private void handleStatusDeletedEvent(StatusDeletedEvent event) { + Pair posAndStatus = findStatusAndPos(event.getStatusId()); + if (posAndStatus == null) return; + + @SuppressWarnings("ConstantConditions") + int pos = posAndStatus.first; + statuses.remove(pos); + adapter.removeItem(pos); + } + + @Nullable + private Pair findStatusAndPos(@NonNull String statusId) { + for (int i = 0; i < statuses.size(); i++) { + if (statusId.equals(statuses.get(i).getId())) { + return new Pair<>(i, statuses.get(i)); + } + } + return null; + } + + private void updateRevealIcon() { + ViewThreadActivity activity = ((ViewThreadActivity) getActivity()); + if (activity == null) return; + + boolean hasAnyWarnings = false; + // Statuses are updated from the main thread so nothing should change while iterating + for (int i = 0; i < statuses.size(); i++) { + if (!TextUtils.isEmpty(statuses.get(i).getSpoilerText())) { + hasAnyWarnings = true; + break; + } + } + if (!hasAnyWarnings) { + activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN); + return; + } + activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE : + ViewThreadActivity.REVEAL_BUTTON_REVEAL); + } + + @Override + protected boolean filterIsRelevant(@NonNull Filter filter) { + return filter.getContext().contains(Filter.THREAD); + } + + @Override + protected void refreshAfterApplyingFilters() { + onRefresh(); + } + + private void setEmojiReactionForStatus(int position, Status status) { + StatusViewData.Concrete newViewData = ViewDataUtils.statusToViewData(status, false, false); + + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); + } + + public void handleEmojiReactEvent(EmojiReactEvent event) { + Pair posAndStatus = findStatusAndPos(event.getNewStatus().getActionableId()); + if (posAndStatus == null) return; + setEmojiReactionForStatus(posAndStatus.first, event.getNewStatus()); + } + + @Override + public void onEmojiReact(final boolean react, final String emoji, final String statusId) { + Pair statusAndPos = findStatusAndPos(statusId); + + if(statusAndPos == null) + return; + int position = statusAndPos.first; + + timelineCases.react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactionForStatus(position, newStatus), + (t) -> Log.d(TAG, + "Failed to react with " + emoji + " on status: " + statusId, t) + ); + + } + + @Override + public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) { + super.emojiReactMenu(statusId, emoji, view, this); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt new file mode 100644 index 0000000..6f3c8ab --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -0,0 +1,207 @@ +/* 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.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent +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.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 lateinit var toolbar: View + private val handler = Handler(Looper.getMainLooper()) + private val hideToolbar = Runnable { + // Hoist toolbar hiding to activity so it can track state across different fragments + // This is explicitly stored as runnable so that we pass it to the handler later for cancellation + mediaActivity.onPhotoTap() + mediaController.hide() + } + private lateinit var mediaActivity: ViewMediaActivity + private val TOOLBAR_HIDE_DELAY_MS = 3000L + private lateinit var mediaController : MediaController + private var isAudio = false + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + // Start/pause/resume video playback as fragment is shown/hidden + super.setUserVisibleHint(isVisibleToUser) + if (videoView == null) { + return + } + + if (isVisibleToUser) { + if (mediaActivity.isToolbarVisible) { + handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) + } + videoView.start() + } else { + handler.removeCallbacks(hideToolbar) + videoView.pause() + mediaController.hide() + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun setupMediaView( + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean + ) { + mediaDescription.text = description + mediaDescription.visible(showingDescription) + + videoView.transitionName = url + videoView.setVideoPath(url) + mediaController = object : MediaController(mediaActivity) { + override fun show(timeout: Int) { + // We're doing manual auto-close management. + // Also, take focus back from the pause button so we can use the back button. + super.show(0) + mediaController.requestFocus() + } + + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + if (event?.keyCode == KeyEvent.KEYCODE_BACK) { + if (event.action == KeyEvent.ACTION_UP) { + hide() + activity?.supportFinishAfterTransition() + } + return true + } + return super.dispatchKeyEvent(event) + } + } + + mediaController.setMediaPlayer(videoView) + videoView.setMediaController(mediaController) + videoView.requestFocus() + videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { + override fun onPause() { + handler.removeCallbacks(hideToolbar) + } + override fun onPlay() { + // Audio doesn't cause the controller to show automatically, + // and we only want to hide the toolbar if it's a video. + if (isAudio) { + mediaController.show() + } else { + hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) + } + } + }) + videoView.setOnPreparedListener { mp -> + val containerWidth = videoContainer.measuredWidth.toFloat() + val containerHeight = 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 + } else { + videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + 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 { _, _ -> + mediaActivity.onPhotoTap() + false + } + + progressBar.hide() + mp.isLooping = true + if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) { + videoView.start() + } + } + + if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) { + mediaActivity.onBringUp() + } + } + + private fun hideToolbarAfterDelay(delayMilliseconds: Long) { + handler.postDelayed(hideToolbar, delayMilliseconds) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + toolbar = activity!!.toolbar + mediaActivity = activity as ViewMediaActivity + return inflater.inflate(R.layout.fragment_view_video, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val attachment = arguments?.getParcelable(ARG_ATTACHMENT) + val url: String + + if (attachment == null) { + throw IllegalArgumentException("attachment has to be set") + } + url = attachment.url + isAudio = attachment.type == Attachment.Type.AUDIO + finalizeViewSetup(url, attachment.previewUrl, attachment.description) + } + + override fun onToolbarVisibilityChange(visible: Boolean) { + if (videoView == null || mediaDescription == null || !userVisibleHint) { + return + } + + isDescriptionVisible = showingDescription && visible + 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) + } + + mediaDescription.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mediaDescription?.visible(isDescriptionVisible) + animation.removeListener(this) + } + }) + .start() + + if (visible && videoView.isPlaying && !isAudio) { + hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) + } else { + handler.removeCallbacks(hideToolbar) + } + } + + override fun onTransitionEnd() { + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java new file mode 100644 index 0000000..c353d0f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java @@ -0,0 +1,23 @@ +/* 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.interfaces; + +public interface AccountActionListener { + void onViewAccount(String id); + void onMute(final boolean mute, final String id, final int position, final boolean notifications); + void onBlock(final boolean block, final String id, final int position); + void onRespondToFollowRequest(final boolean accept, final String id, final int position); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt new file mode 100644 index 0000000..04b1ebd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt @@ -0,0 +1,22 @@ +/* Copyright 2019 Levi Bard + * + * 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.interfaces + +import com.keylesspalace.tusky.db.AccountEntity + +interface AccountSelectionListener { + fun onAccountSelected(account: AccountEntity) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java new file mode 100644 index 0000000..023ddde --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java @@ -0,0 +1,28 @@ +/* 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.interfaces; + +import androidx.annotation.Nullable; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +public interface ActionButtonActivity { + + /* return the ActionButton of the Activity to hide or show it on scroll */ + @Nullable + FloatingActionButton getActionButton(); + + default void onActionButtonHidden() {} +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt new file mode 100644 index 0000000..0a25b58 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.interfaces + +import android.view.View +import com.keylesspalace.tusky.entity.Chat + +interface ChatActionListener: LinkListener { + fun onLoadMore(position: Int) {} + fun onMore(chatId: String, v: View) {} + fun openChat(position: Int) {} + fun onViewMedia(position: Int, view: View?) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java new file mode 100644 index 0000000..90599b2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java @@ -0,0 +1,22 @@ +/* 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.interfaces; + +public interface LinkListener { + void onViewTag(String tag); + void onViewAccount(String id); + void onViewUrl(String url); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java new file mode 100644 index 0000000..ca83e08 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java @@ -0,0 +1,5 @@ +package com.keylesspalace.tusky.interfaces; + +public interface PermissionRequester { + void onRequestPermissionsResult(String[] permissions, int[] grantResults); +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt new file mode 100644 index 0000000..5032774 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.interfaces + +/** + * Created by pandasoft (joelpyska1@gmail.com) on 04/04/2019. + */ +interface RefreshableFragment { + /** + * Call this method to refresh fragment content + */ + fun refreshContent() +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt new file mode 100644 index 0000000..c50178c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.interfaces + +/** + * Created by pandasoft (joelpyska1@gmail.com) on 04/04/2019. + */ +interface ReselectableFragment { + /** + * Call this method when tab reselected + */ + fun onReselect() +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java new file mode 100644 index 0000000..d956cec --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -0,0 +1,72 @@ +/* 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.interfaces; + +import android.view.View; + +import java.util.List; +import com.keylesspalace.tusky.entity.EmojiReaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface StatusActionListener extends LinkListener { + void onReply(int position); + void onReblog(final boolean reblog, final int position); + void onFavourite(final boolean favourite, final int position); + void onBookmark(final boolean bookmark, final int position); + void onMore(@NonNull View view, final int position); + void onViewMedia(int position, int attachmentIndex, @Nullable View view); + void onViewThread(int position); + void onViewReplyTo(int position); + + /** + * Open reblog author for the status. + * @param position At which position in the list status is located + */ + void onOpenReblog(int position); + void onExpandedChange(boolean expanded, int position); + void onContentHiddenChange(boolean isShowing, int position); + void onLoadMore(int position); + + /** + * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long + * status content is interacted with. + * + * @param isCollapsed Whether the status content is shown in a collapsed state or fully. + * @param position The position of the status in the list. + */ + void onContentCollapsedChange(boolean isCollapsed, int position); + + /** + * called when the reblog count has been clicked + * @param position The position of the status in the list. + */ + default void onShowReblogs(int position) {} + + /** + * called when the favourite count has been clicked + * @param position The position of the status in the list. + */ + default void onShowFavs(int position) {} + + void onVoteInPoll(int position, @NonNull List choices); + + default void onMute(int position, boolean isMuted) {} + default void onEmojiReact(@NonNull final boolean react, @NonNull final String emoji, @NonNull final String statusId) {}; + default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, @NonNull final String statusId) {}; + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt new file mode 100644 index 0000000..6eabea5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt @@ -0,0 +1,37 @@ +/* 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.json + +import android.text.Spanned +import android.text.SpannedString +import androidx.core.text.HtmlCompat +import androidx.core.text.parseAsHtml +import com.google.gson.* +import com.keylesspalace.tusky.util.trimTrailingWhitespace +import java.lang.reflect.Type + +class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned { + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * all status contents do, so it should be trimmed. */ + return json.asString?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString("") + } + + override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return JsonPrimitive(HtmlCompat.toHtml(src!!, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java new file mode 100644 index 0000000..2dcedd8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java @@ -0,0 +1,76 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.network; + +import androidx.annotation.NonNull; + +import com.keylesspalace.tusky.db.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; + +import java.io.IOException; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by charlag on 31/10/17. + */ + +public final class InstanceSwitchAuthInterceptor implements Interceptor { + private AccountManager accountManager; + + public InstanceSwitchAuthInterceptor(AccountManager accountManager) { + this.accountManager = accountManager; + } + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + + Request originalRequest = chain.request(); + + // only switch domains if the request comes from retrofit + if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) { + AccountEntity currentAccount = accountManager.getActiveAccount(); + + Request.Builder builder = originalRequest.newBuilder(); + + String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER); + if (instanceHeader != null) { + // use domain explicitly specified in custom header + builder.url(swapHost(originalRequest.url(), instanceHeader)); + builder.removeHeader(MastodonApi.DOMAIN_HEADER); + } else if (currentAccount != null) { + //use domain of current account + builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) + .header("Authorization", + String.format("Bearer %s", currentAccount.getAccessToken())); + } + Request newRequest = builder.build(); + + return chain.proceed(newRequest); + + } else { + return chain.proceed(originalRequest); + } + } + + @NonNull + private static HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) { + return url.newBuilder().host(host).build(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt new file mode 100644 index 0000000..dda8201 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -0,0 +1,684 @@ +/* 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.network + +import com.keylesspalace.tusky.entity.* +import io.reactivex.Completable +import io.reactivex.Single +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.* +import retrofit2.http.Field + +/** + * for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/ + */ + +@JvmSuppressWildcards +interface MastodonApi { + + companion object { + const val ENDPOINT_AUTHORIZE = "/oauth/authorize" + const val DOMAIN_HEADER = "domain" + const val PLACEHOLDER_DOMAIN = "dummy.placeholder" + } + + @GET("/api/v1/lists") + fun getLists(): Single> + + @GET("/api/v1/custom_emojis") + fun getCustomEmojis(): Single> + + @GET("api/v1/instance") + fun getInstance(): Single + + @GET("api/v1/filters") + fun getFilters(): Call> + + @GET("api/v1/timelines/home?with_muted=true") + fun homeTimeline( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/timelines/home?with_muted=true") + fun homeTimelineSingle( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Single> + + @GET("api/v1/timelines/public?with_muted=true") + fun publicTimeline( + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/timelines/tag/{hashtag}?with_muted=true") + fun hashtagTimeline( + @Path("hashtag") hashtag: String, + @Query("any[]") any: List?, + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/timelines/list/{listId}?with_muted=true") + fun listTimeline( + @Path("listId") listId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/notifications") + fun notifications( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_types[]") excludes: Set?, + @Query("with_muted") withMuted: Boolean? + ): Call> + + @GET("api/v1/markers") + fun markersWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("timeline[]") timelines: List + ): Single> + + @GET("api/v1/notifications?with_muted=true") + fun notificationsWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("since_id") sinceId: String?, + @Query("include_types[]") includeTypes: List? + ): Single> + + @POST("api/v1/notifications/clear") + fun clearNotifications(): Call + + @GET("api/v1/notifications/{id}") + fun notification( + @Path("id") notificationId: String + ): Call + + @Multipart + @POST("api/v1/media") + fun uploadMedia( + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null + ): Single + + @FormUrlEncoded + @PUT("api/v1/media/{mediaId}") + fun updateMedia( + @Path("mediaId") mediaId: String, + @Field("description") description: String + ): Single + + @POST("api/v1/statuses") + fun createStatus( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body status: NewStatus + ): Call + + @GET("api/v1/statuses/{id}") + fun status( + @Path("id") statusId: String + ): Call + + @GET("api/v1/statuses/{id}") + fun statusSingle( + @Path("id") statusId: String + ): Single + + @GET("api/v1/statuses/{id}/context") + fun statusContext( + @Path("id") statusId: String + ): Call + + @GET("api/v1/statuses/{id}/reblogged_by") + fun statusRebloggedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/statuses/{id}/favourited_by") + fun statusFavouritedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Single>> + + @DELETE("api/v1/statuses/{id}") + fun deleteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/reblog") + fun reblogStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unreblog") + fun unreblogStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/favourite") + fun favouriteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unfavourite") + fun unfavouriteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/bookmark") + fun bookmarkStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unbookmark") + fun unbookmarkStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/pin") + fun pinStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unpin") + fun unpinStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/mute") + fun muteConversation( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unmute") + fun unmuteConversation( + @Path("id") statusId: String + ): Single + + @GET("api/v1/scheduled_statuses") + fun scheduledStatuses( + @Query("limit") limit: Int? = null, + @Query("max_id") maxId: String? = null + ): Single> + + @DELETE("api/v1/scheduled_statuses/{id}") + fun deleteScheduledStatus( + @Path("id") scheduledStatusId: String + ): Single + + @GET("api/v1/accounts/verify_credentials") + fun accountVerifyCredentials(): Single + + @FormUrlEncoded + @PATCH("api/v1/accounts/update_credentials") + fun accountUpdateSource( + @Field("source[privacy]") privacy: String?, + @Field("source[sensitive]") sensitive: Boolean? + ): Call + + @Multipart + @PATCH("api/v1/accounts/update_credentials") + fun accountUpdateCredentials( + @Part(value = "display_name") displayName: RequestBody?, + @Part(value = "note") note: RequestBody?, + @Part(value = "locked") locked: RequestBody?, + @Part avatar: MultipartBody.Part?, + @Part header: MultipartBody.Part?, + @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?, + @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?, + @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?, + @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?, + @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?, + @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, + @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, + @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? + ): Call + + @GET("api/v1/accounts/search") + fun searchAccounts( + @Query("q") query: String, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("following") following: Boolean? = null + ): Single> + + @GET("api/v1/accounts/{id}") + fun account( + @Path("id") accountId: String + ): Single + + /** + * Method to fetch statuses for the specified account. + * @param accountId ID for account for which statuses will be requested + * @param maxId Only statuses with ID less than maxID will be returned + * @param sinceId Only statuses with ID bigger than sinceID will be returned + * @param limit Limit returned statuses (current API limits: default - 20, max - 40) + * @param excludeReplies only return statuses that are no replies + * @param onlyMedia only return statuses that have media attached + */ + @GET("api/v1/accounts/{id}/statuses?with_muted=true") + fun accountStatuses( + @Path("id") accountId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_replies") excludeReplies: Boolean?, + @Query("only_media") onlyMedia: Boolean?, + @Query("pinned") pinned: Boolean? + ): Call> + + @GET("api/v1/accounts/{id}/followers") + fun accountFollowers( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/accounts/{id}/following") + fun accountFollowing( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Single>> + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/follow") + fun followAccount( + @Path("id") accountId: String, + @Field("reblogs") showReblogs: Boolean? = null, + @Field("notify") notify: Boolean? = null + ): Single + + @POST("api/v1/accounts/{id}/unfollow") + fun unfollowAccount( + @Path("id") accountId: String + ): Single + + @POST("api/v1/accounts/{id}/block") + fun blockAccount( + @Path("id") accountId: String + ): Single + + @POST("api/v1/accounts/{id}/unblock") + fun unblockAccount( + @Path("id") accountId: String + ): Single + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/mute") + fun muteAccount( + @Path("id") accountId: String, + @Field("notifications") notifications: Boolean? = null, + @Field("duration") duration: Int? = null + ): Single + + @POST("api/v1/accounts/{id}/unmute") + fun unmuteAccount( + @Path("id") accountId: String + ): Single + + @GET("api/v1/accounts/relationships") + fun relationships( + @Query("id[]") accountIds: List + ): Single> + + @GET("api/v1/accounts/{id}/identity_proofs") + fun identityProofs( + @Path("id") accountId: String + ): Single> + + @POST("api/v1/pleroma/accounts/{id}/subscribe") + fun subscribeAccount( + @Path("id") accountId: String + ): Single + + @POST("api/v1/pleroma/accounts/{id}/unsubscribe") + fun unsubscribeAccount( + @Path("id") accountId: String + ): Single + + @GET("api/v1/blocks") + fun blocks( + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/mutes") + fun mutes( + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/domain_blocks") + fun domainBlocks( + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null + ): Single>> + + @FormUrlEncoded + @POST("api/v1/domain_blocks") + fun blockDomain( + @Field("domain") domain: String + ): Call + + @FormUrlEncoded + // @DELETE doesn't support fields + @HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true) + fun unblockDomain(@Field("domain") domain: String): Call + + @GET("api/v1/favourites?with_muted=true") + fun favourites( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/bookmarks?with_muted=true") + fun bookmarks( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/follow_requests") + fun followRequests( + @Query("max_id") maxId: String? + ): Single>> + + @POST("api/v1/follow_requests/{id}/authorize") + fun authorizeFollowRequest( + @Path("id") accountId: String + ): Call + + @POST("api/v1/follow_requests/{id}/reject") + fun rejectFollowRequest( + @Path("id") accountId: String + ): Call + + @POST("api/v1/follow_requests/{id}/authorize") + fun authorizeFollowRequestObservable( + @Path("id") accountId: String + ): Single + + @POST("api/v1/follow_requests/{id}/reject") + fun rejectFollowRequestObservable( + @Path("id") accountId: String + ): Single + + @FormUrlEncoded + @POST("api/v1/apps") + fun authenticateApp( + @Header(DOMAIN_HEADER) domain: String, + @Field("client_name") clientName: String, + @Field("redirect_uris") redirectUris: String, + @Field("scopes") scopes: String, + @Field("website") website: String + ): Call + + @FormUrlEncoded + @POST("oauth/token") + fun fetchOAuthToken( + @Header(DOMAIN_HEADER) domain: String, + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("redirect_uri") redirectUri: String, + @Field("code") code: String, + @Field("grant_type") grantType: String + ): Call + + @FormUrlEncoded + @POST("api/v1/lists") + fun createList( + @Field("title") title: String + ): Single + + @FormUrlEncoded + @PUT("api/v1/lists/{listId}") + fun updateList( + @Path("listId") listId: String, + @Field("title") title: String + ): Single + + @DELETE("api/v1/lists/{listId}") + fun deleteList( + @Path("listId") listId: String + ): Completable + + @GET("api/v1/lists/{listId}/accounts") + fun getAccountsInList( + @Path("listId") listId: String, + @Query("limit") limit: Int + ): Single> + + @FormUrlEncoded + // @DELETE doesn't support fields + @HTTP(method = "DELETE", path = "api/v1/lists/{listId}/accounts", hasBody = true) + fun deleteAccountFromList( + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List + ): Completable + + @FormUrlEncoded + @POST("api/v1/lists/{listId}/accounts") + fun addCountToList( + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List + ): Completable + + @GET("/api/v1/conversations") + fun getConversations( + @Query("max_id") maxId: String? = null, + @Query("limit") limit: Int + ): Call> + + data class PostFilter( + val phrase: String, + val context: List, + val irreversible: Boolean?, + val whole_word: Boolean?, + val expires_in: String? + ); + + @POST("api/v1/filters") + fun createFilter(@Body body: PostFilter): Call + + @PUT("api/v1/filters/{id}") + fun updateFilter( + @Path("id") id: String, + @Body body: PostFilter + ): Call + + @DELETE("api/v1/filters/{id}") + fun deleteFilter( + @Path("id") id: String + ): Call + + @FormUrlEncoded + @POST("api/v1/polls/{id}/votes") + fun voteInPoll( + @Path("id") id: String, + @Field("choices[]") choices: List + ): Single + + @GET("api/v1/announcements") + fun listAnnouncements( + @Query("with_dismissed") withDismissed: Boolean = true + ): Single> + + @POST("api/v1/announcements/{id}/dismiss") + fun dismissAnnouncement( + @Path("id") announcementId: String + ): Single + + @PUT("api/v1/announcements/{id}/reactions/{name}") + fun addAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): Single + + @DELETE("api/v1/announcements/{id}/reactions/{name}") + fun removeAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): Single + + @FormUrlEncoded + @POST("api/v1/reports") + fun reportObservable( + @Field("account_id") accountId: String, + @Field("status_ids[]") statusIds: List, + @Field("comment") comment: String, + @Field("forward") isNotifyRemote: Boolean? + ): Single + + @GET("api/v1/accounts/{id}/statuses?with_muted=true") + fun accountStatusesObservable( + @Path("id") accountId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_reblogs") excludeReblogs: Boolean? + ): Single> + + @GET("api/v1/statuses/{id}") + fun statusObservable( + @Path("id") statusId: String + ): Single + + @GET("api/v2/search") + fun searchObservable( + @Query("q") query: String?, + @Query("type") type: String? = null, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + @Query("following") following: Boolean? = null + ): Single + + @GET(".well-known/nodeinfo") + fun getNodeinfoLinks() : Single + + @GET + fun getNodeinfo(@Url url: String) : Single + + @PUT("api/v1/pleroma/statuses/{id}/reactions/{emoji}") + fun reactWithEmoji( + @Path("id") statusId: String, + @Path("emoji") emoji: String + ): Single + + @DELETE("api/v1/pleroma/statuses/{id}/reactions/{emoji}") + fun unreactWithEmoji( + @Path("id") statusId: String, + @Path("emoji") emoji: String + ): Single + + @GET("api/v1/pleroma/statuses/{id}/reactions/{emoji}") + fun statusReactedBy( + @Path("id") statusId: String, + @Path("emoji") emoji: String + ): Single>> + + // NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS + // just for testing and because puniko asked me + @GET("static/stickers.json") + fun getStickers() : Single> + + @GET + fun getStickerPack( + @Url path: String + ): Single> + // NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS + + @POST("api/v1/pleroma/chats/{id}/messages/{message_id}/read") + fun markChatMessageAsRead( + @Path("id") chatId: String, + @Path("message_id") messageId: String + ): Single + + @DELETE("api/v1/pleroma/chats/{id}/messages/{message_id}") + fun deleteChatMessage( + @Path("id") chatId: String, + @Path("message_id") messageId: String + ): Single + + @GET("api/v1/pleroma/chats") + fun getChats( + @Query("max_id") maxId: String?, + @Query("min_id") minId: String?, + @Query("since_id") sinceId: String?, + @Query("offset") offset: Int?, + @Query("limit") limit: Int? + ): Single> + + @GET("api/v1/pleroma/chats/{id}/messages") + fun getChatMessages( + @Path("id") chatId: String, + @Query("max_id") maxId: String?, + @Query("min_id") minId: String?, + @Query("since_id") sinceId: String?, + @Query("offset") offset: Int?, + @Query("limit") limit: Int? + ): Single> + + @POST("api/v1/pleroma/chats/{id}/messages") + fun createChatMessage( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Path("id") chatId: String, + @Body chatMessage: NewChatMessage + ): Call + + @FormUrlEncoded + @POST("api/v1/pleroma/chats/{id}/read") + fun markChatAsRead( + @Path("id") chatId: String, + @Field("last_read_id") lastReadId: String? = null + ): Single + + @POST("api/v1/pleroma/chats/by-account-id/{id}") + fun createChat( + @Path("id") accountId: String + ): Single + + @GET("api/v1/pleroma/chats/{id}") + fun getChat( + @Path("id") chatId: String + ): Single + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/note") + fun updateAccountNote( + @Path("id") accountId: String, + @Field("comment") note: String + ): Single +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java b/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java new file mode 100644 index 0000000..d559d35 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java @@ -0,0 +1,74 @@ +/* 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.network; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; + +public final class ProgressRequestBody extends RequestBody { + private final InputStream content; + private final long contentLength; + private final UploadCallback uploadListener; + private final MediaType mediaType; + + private static final int DEFAULT_BUFFER_SIZE = 2048; + + public interface UploadCallback { + void onProgressUpdate(int percentage); + } + + public ProgressRequestBody(final InputStream content, long contentLength, final MediaType mediaType, final UploadCallback listener) { + this.content = content; + this.contentLength = contentLength; + this.mediaType = mediaType; + this.uploadListener = listener; + } + + @Override + public MediaType contentType() { + return mediaType; + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public void writeTo(@NonNull BufferedSink sink) throws IOException { + + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long uploaded = 0; + + try { + int read; + while ((read = content.read(buffer)) != -1) { + uploadListener.onProgressUpdate((int)(100 * uploaded / contentLength)); + + uploaded += read; + sink.write(buffer, 0, read); + } + } finally { + content.close(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt new file mode 100644 index 0000000..f138749 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -0,0 +1,168 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import java.lang.IllegalStateException +import android.util.Log + +/** + * Created by charlag on 3/24/18. + */ + +interface TimelineCases { + fun reblog(status: Status, reblog: Boolean): Single + fun favourite(status: Status, favourite: Boolean): Single + fun bookmark(status: Status, bookmark: Boolean): Single + fun muteConversation(status: Status, mute: Boolean) + fun mute(id: String, notifications: Boolean, duration: Int) + fun block(id: String) + fun delete(id: String): Single + fun pin(status: Status, pin: Boolean) + fun voteInPoll(status: Status, choices: List): Single + fun react(emoji: String, id: String, react: Boolean) : Single +} + +class TimelineCasesImpl( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : TimelineCases { + + /** + * Unused yet but can be use for cancellation later. It's always a good idea to save + * Disposables. + */ + private val cancelDisposable = CompositeDisposable() + + override fun reblog(status: Status, reblog: Boolean): Single { + val id = status.actionableId + + val call = if (reblog) { + mastodonApi.reblogStatus(id) + } else { + mastodonApi.unreblogStatus(id) + } + return call.doAfterSuccess { + eventHub.dispatch(ReblogEvent(status.id, reblog)) + } + } + + override fun favourite(status: Status, favourite: Boolean): Single { + val id = status.actionableId + + val call = if (favourite) { + mastodonApi.favouriteStatus(id) + } else { + mastodonApi.unfavouriteStatus(id) + } + return call.doAfterSuccess { + eventHub.dispatch(FavoriteEvent(status.id, favourite)) + } + } + + override fun bookmark(status: Status, bookmark: Boolean): Single { + val id = status.actionableId + + val call = if (bookmark) { + mastodonApi.bookmarkStatus(id) + } else { + mastodonApi.unbookmarkStatus(id) + } + return call.doAfterSuccess { + eventHub.dispatch(BookmarkEvent(status.id, bookmark)) + } + } + + override fun muteConversation(status: Status, mute: Boolean) { + val id = status.actionableId + if (mute) { + mastodonApi.muteConversation(id) + } else { + mastodonApi.unmuteConversation(id) + } + .subscribe({ + eventHub.dispatch(MuteConversationEvent(id, mute)) + }, { t -> + Log.w("Failed to mute status", t) + }) + .addTo(cancelDisposable) + } + + override fun mute(id: String, notifications: Boolean, duration: Int) { + mastodonApi.muteAccount(id, notifications, duration) + .subscribe({ + eventHub.dispatch(MuteEvent(id, true)) + }, { t -> + Log.w("Failed to mute account", t) + }) + .addTo(cancelDisposable) + } + + override fun block(id: String) { + mastodonApi.blockAccount(id) + .subscribe({ + eventHub.dispatch(BlockEvent(id)) + }, { t -> + Log.w("Failed to block account", t) + }) + .addTo(cancelDisposable) + } + + override fun delete(id: String): Single { + return mastodonApi.deleteStatus(id) + .doAfterSuccess { + eventHub.dispatch(StatusDeletedEvent(id)) + } + } + + override fun pin(status: Status, pin: Boolean) { + // Replace with extension method if we use RxKotlin + (if (pin) mastodonApi.pinStatus(status.id) else mastodonApi.unpinStatus(status.id)) + .subscribe({ updatedStatus -> + status.pinned = updatedStatus.pinned + }, {}) + .addTo(this.cancelDisposable) + } + + override fun voteInPoll(status: Status, choices: List): Single { + val pollId = status.actionableStatus.poll?.id + + if(pollId == null || choices.isEmpty()) { + return Single.error(IllegalStateException()) + } + + return mastodonApi.voteInPoll(pollId, choices).doAfterSuccess { + eventHub.dispatch(PollVoteEvent(status.id, it)) + } + } + + override fun react(emoji: String, id: String, react: Boolean): Single { + val call = if (react) { + mastodonApi.reactWithEmoji(id, emoji) + } else { + mastodonApi.unreactWithEmoji(id, emoji) + } + return call.doAfterSuccess { status -> + eventHub.dispatch(EmojiReactEvent(status)) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt new file mode 100644 index 0000000..f8c026e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt @@ -0,0 +1,55 @@ +/* Copyright 2019 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.pager + +import androidx.fragment.app.* + +import com.keylesspalace.tusky.fragment.AccountMediaFragment +import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.interfaces.RefreshableFragment + +import com.keylesspalace.tusky.util.CustomFragmentStateAdapter + +class AccountPagerAdapter( + activity: FragmentActivity, + private val accountId: String +) : CustomFragmentStateAdapter(activity) { + + override fun getItemCount() = TAB_COUNT + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId, false) + 1 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId, false) + 2 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId, false) + 3 -> AccountMediaFragment.newInstance(accountId, false) + else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") + } + } + + fun refreshContent() { + for (i in 0 until TAB_COUNT) { + val fragment = getFragment(i) + if (fragment != null && fragment is RefreshableFragment) { + (fragment as RefreshableFragment).refreshContent() + } + } + } + + companion object { + private const val TAB_COUNT = 4 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt new file mode 100644 index 0000000..a3dfcf2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt @@ -0,0 +1,25 @@ +package com.keylesspalace.tusky.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.ViewMediaAdapter +import com.keylesspalace.tusky.fragment.ViewMediaFragment + +class AvatarImagePagerAdapter( + activity: FragmentActivity, + private val avatarUrl: String +) : ViewMediaAdapter(activity) { + + override fun createFragment(position: Int): Fragment { + return if (position == 0) { + ViewMediaFragment.newAvatarInstance(avatarUrl) + } else { + throw IllegalStateException() + } + } + + override fun getItemCount() = 1 + + override fun onTransitionEnd(position: Int) { + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt new file mode 100644 index 0000000..4f813d8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt @@ -0,0 +1,42 @@ +package com.keylesspalace.tusky.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.ViewMediaAdapter +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.fragment.ViewMediaFragment +import java.lang.ref.WeakReference + +class ImagePagerAdapter( + activity: FragmentActivity, + private val attachments: List, + private val initialPosition: Int +) : ViewMediaAdapter(activity) { + + private var didTransition = false + private val fragments = MutableList?>(attachments.size) { null } + + override fun getItemCount() = attachments.size + + override fun createFragment(position: Int): Fragment { + if (position >= 0 && position < attachments.size) { + // Fragment should not wait for or start transition if it already happened but we + // instantiate the same fragment again, e.g. open the first photo, scroll to the + // forth photo and then back to the first. The first fragment will try to start the + // transition and wait until it's over and it will never take place. + val fragment = ViewMediaFragment.newInstance( + attachment = attachments[position], + shouldStartPostponedTransition = !didTransition && position == initialPosition + ) + fragments[position] = WeakReference(fragment) + return fragment + } else { + throw IllegalStateException() + } + } + + override fun onTransitionEnd(position: Int) { + this.didTransition = true + fragments[position]?.get()?.onTransitionEnd() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt new file mode 100644 index 0000000..1e10294 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt @@ -0,0 +1,32 @@ +/* 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.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.util.CustomFragmentStateAdapter + +class MainPagerAdapter(val tabs: List, activity: FragmentActivity) : CustomFragmentStateAdapter(activity) { + + override fun createFragment(position: Int): Fragment { + val tab = tabs[position] + return tab.fragment(tab.arguments) + } + + override fun getItemCount() = tabs.size + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt new file mode 100644 index 0000000..d9b9485 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt @@ -0,0 +1,44 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import dagger.android.AndroidInjection +import javax.inject.Inject + +class NotificationClearBroadcastReceiver : BroadcastReceiver() { + + @Inject + lateinit var accountManager: AccountManager + + override fun onReceive(context: Context, intent: Intent) { + AndroidInjection.inject(this, context) + + val accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1) + + val account = accountManager.getAccountById(accountId) + if (account != null) { + account.activeNotifications = "[]" + accountManager.saveAccount(account) + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt new file mode 100644 index 0000000..2f59a58 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -0,0 +1,167 @@ +/* Copyright 2018 Jeremiasz Nelz + * + * 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.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Message +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.service.SendTootService +import com.keylesspalace.tusky.service.TootToSend +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.service.MessageToSend +import com.keylesspalace.tusky.util.randomAlphanumericString +import dagger.android.AndroidInjection +import javax.inject.Inject + +private const val TAG = "SendStatusBR" + +class SendStatusBroadcastReceiver : BroadcastReceiver() { + + @Inject + lateinit var accountManager: AccountManager + + override fun onReceive(context: Context, intent: Intent) { + AndroidInjection.inject(this, context) + + val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1) + val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1) + val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER) + val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME) + val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) + val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility + val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) + val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) + val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT) + val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL) + val chatId = intent.getStringExtra(NotificationHelper.KEY_CHAT_ID) + + val account = accountManager.getAccountById(senderId) + + val notificationManager = NotificationManagerCompat.from(context) + + if (account == null) { + Log.w(TAG, "Account \"$senderId\" not found in database. Aborting quick reply!") + + val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + + builder.setContentTitle(context.getString(R.string.error_generic)) + builder.setContentText(context.getString(R.string.error_sender_account_gone)) + + builder.setSubText(senderFullName) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + builder.setOnlyAlertOnce(true) + + notificationManager.notify(notificationId, builder.build()) + return + } + + if (intent.action == NotificationHelper.COMPOSE_ACTION) { + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + + notificationManager.cancel(notificationId) + + accountManager.setActiveAccount(senderId) + + val composeIntent = ComposeActivity.startIntent(context, ComposeOptions( + inReplyToId = citedStatusId, + replyVisibility = visibility, + contentWarning = spoiler, + mentionedUsernames = mentions.toSet(), + replyingStatusAuthor = localAuthorId, + replyingStatusContent = citedText + )) + + composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + context.startActivity(composeIntent) + } else { + val message = getReplyMessage(intent) + + val sendIntent = if(intent.action == NotificationHelper.REPLY_ACTION) { + val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString() + + SendTootService.sendTootIntent( + context, + TootToSend( + text = text, + warningText = spoiler, + visibility = visibility.serverString(), + sensitive = false, + mediaIds = emptyList(), + mediaUris = emptyList(), + mediaDescriptions = emptyList(), + scheduledAt = null, + inReplyToId = citedStatusId, + poll = null, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + formattingSyntax = "", + preview = false, + accountId = account.id, + savedTootUid = -1, + draftId = -1, + idempotencyKey = randomAlphanumericString(16), + retries = 0 + ) + ) + } else { + SendTootService.sendMessageIntent(context, + MessageToSend(message.toString(), null, null, account.id, chatId!!, 0)) + } + + context.startService(sendIntent) + + val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + + builder.setContentTitle(context.getString(R.string.status_sent)) + builder.setContentText(context.getString(R.string.status_sent_long)) + + builder.setSubText(senderFullName) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + builder.setOnlyAlertOnce(true) + + notificationManager.notify(notificationId, builder.build()) + } + } + + private fun getReplyMessage(intent: Intent): CharSequence { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + + return remoteInput.getCharSequence(NotificationHelper.KEY_REPLY, "") + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt new file mode 100644 index 0000000..d96df4c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt @@ -0,0 +1,264 @@ +package com.keylesspalace.tusky.repository + +import android.text.SpannedString +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.keylesspalace.tusky.db.* +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK +import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.trimTrailingWhitespace +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.IOException +import java.util.* + +typealias ChatStatus = Either +typealias ChatMesssageOrPlaceholder = Either + +interface ChatRepository { + fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, + requestMode: TimelineRequestMode): Single> + + fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single> +} + +class ChatRepositoryImpl( + private val chatsDao: ChatsDao, + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val gson: Gson +) : ChatRepository { + + override fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, + limit: Int, requestMode: TimelineRequestMode + ): Single> { + val acc = accountManager.activeAccount ?: throw IllegalStateException() + val accountId = acc.id + + return if (requestMode == DISK) { + this.getChatsFromDb(accountId, maxId, sinceId, limit) + } else { + getChatsFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + } + } + + override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single> { + val acc = accountManager.activeAccount ?: throw IllegalStateException() + val accountId = acc.id + + /*return if (requestMode == DISK) { + getChatMessagesFromDb(chatId, accountId, maxId, sinceId, limit) + } else { + getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + }*/ + + return getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + } + + private fun getChatsFromNetwork(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, limit: Int, + accountId: Long, requestMode: TimelineRequestMode + ): Single> { + return mastodonApi.getChats(maxId, null, sinceIdMinusOne, 0, limit + 1) + .map { chats -> + this.saveChatsToDb(accountId, chats, maxId, sinceId) + } + .flatMap { chats -> + this.addFromDbIfNeeded(accountId, chats, maxId, sinceId, limit, requestMode) + } + .onErrorResumeNext { error -> + if (error is IOException && requestMode != NETWORK) { + this.getChatsFromDb(accountId, maxId, sinceId, limit) + } else { + Single.error(error) + } + } + } + + private fun getChatMessagesFromNetwork(chatId: String, maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, limit: Int, + accountId: Long, requestMode: TimelineRequestMode + ): Single> { + return mastodonApi.getChatMessages(chatId, maxId, null, sinceIdMinusOne, 0, limit + 1).map { + it.mapTo(mutableListOf(), ChatMessage::lift) + } + } + + + private fun addFromDbIfNeeded(accountId: Long, chats: List, + maxId: String?, sinceId: String?, limit: Int, + requestMode: TimelineRequestMode + ): Single> { + return if (requestMode != NETWORK && chats.size < 2) { + val newMaxID = if (chats.isEmpty()) { + maxId + } else { + chats.last { it.isRight() }.asRight().id + } + this.getChatsFromDb(accountId, newMaxID, sinceId, limit) + .map { fromDb -> + // If it's just placeholders and less than limit (so we exhausted both + // db and server at this point) + if (fromDb.size < limit && fromDb.all { !it.isRight() }) { + chats + } else { + chats + fromDb + } + } + } else { + Single.just(chats) + } + } + + private fun getChatsFromDb(accountId: Long, maxId: String?, sinceId: String?, + limit: Int): Single> { + return chatsDao.getChatsForAccount(accountId, maxId, sinceId, limit) + .subscribeOn(Schedulers.io()) + .map { chats -> + chats.map { it.toChat(gson) } + } + } + + + private fun saveChatsToDb(accountId: Long, chats: List, + maxId: String?, sinceId: String? + ): List { + var placeholderToInsert: Placeholder? = null + + // Look for overlap + val resultChats = if (chats.isNotEmpty() && sinceId != null) { + val indexOfSince = chats.indexOfLast { it.id == sinceId } + if (indexOfSince == -1) { + // We didn't find the status which must be there. Add a placeholder + placeholderToInsert = Placeholder(sinceId.inc()) + chats.mapTo(mutableListOf(), Chat::lift) + .apply { + add(Either.Left(placeholderToInsert)) + } + } else { + // There was an overlap. Remove all overlapped statuses. No need for a placeholder. + chats.mapTo(mutableListOf(), Chat::lift) + .apply { + subList(indexOfSince, size).clear() + } + } + } else { + // Just a normal case. + chats.map(Chat::lift) + } + + Single.fromCallable { + + if(chats.isNotEmpty()) { + chatsDao.deleteRange(accountId, chats.last().id, chats.first().id) + } + + for (chat in chats) { + val pair = chat.toEntity(accountId, gson) + + chatsDao.insertInTransaction( + pair.first, + pair.second, + chat.account.toEntity(accountId, gson) + ) + } + + placeholderToInsert?.let { + chatsDao.insertChatIfNotThere(it.toChatEntity(accountId)) + } + + // If we're loading in the bottom insert placeholder after every load + // (for requests on next launches) but not return it. + if (sinceId == null && chats.isNotEmpty()) { + chatsDao.insertChatIfNotThere( + Placeholder(chats.last().id.dec()).toChatEntity(accountId)) + } + + // There may be placeholders which we thought could be from our TL but they are not + if (chats.size > 2) { + chatsDao.removeAllPlaceholdersBetween(accountId, chats.first().id, + chats.last().id) + } else if (placeholderToInsert == null && maxId != null && sinceId != null) { + chatsDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId) + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + + return resultChats + } +} + +private val emojisListTypeToken = object : TypeToken>() {} + +fun Placeholder.toChatEntity(timelineUserId: Long): ChatEntity { + return ChatEntity( + localId = timelineUserId, + chatId = this.id, + accountId = "", + unread = 0L, + updatedAt = 0L, + lastMessageId = null + ) +} + +fun ChatMessage.toEntity(timelineUserId: Long, gson: Gson) : ChatMessageEntity { + return ChatMessageEntity( + localId = timelineUserId, + messageId = this.id, + content = this.content?.toHtml(), + chatId = this.chatId, + accountId = this.accountId, + createdAt = this.createdAt.time, + attachment = this.attachment?.let { gson.toJson(it, Attachment::class.java) }, + emojis = gson.toJson(this.emojis) + ) +} + +fun Chat.toEntity(timelineUserId: Long, gson: Gson): Pair { + return Pair(ChatEntity( + localId = timelineUserId, + chatId = this.id, + accountId = this.account.id, + unread = this.unread, + updatedAt = this.updatedAt.time, + lastMessageId = this.lastMessage?.id + ), this.lastMessage?.toEntity(timelineUserId, gson)) +} + +fun ChatMessageEntity.toChatMessage(gson: Gson) : ChatMessage { + return ChatMessage( + id = this.messageId, + content = this.content?.let { it.parseAsHtml().trimTrailingWhitespace() }, + chatId = this.chatId, + accountId = this.accountId, + createdAt = Date(this.createdAt), + attachment = this.attachment?.let { gson.fromJson(it, Attachment::class.java) }, + emojis = gson.fromJson(this.emojis, object : TypeToken>() {}.type ), + card = null /* don't care about card */ + ) +} + +fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus { + if(account == null || chat.accountId.isEmpty() || chat.updatedAt == 0L) + return Either.Left(Placeholder(chat.chatId)) + + return Chat( + account = this.account?.toAccount(gson) ?: Account("", "", "", "", SpannedString(""), "", "", "" ), + id = this.chat.chatId, + unread = this.chat.unread, + updatedAt = Date(this.chat.updatedAt), + lastMessage = this.lastMessage?.toChatMessage(gson) + ).lift() +} + +fun ChatMessage.lift(): ChatMesssageOrPlaceholder = Either.Right(this) + +fun Chat.lift(): ChatStatus = Either.Right(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt new file mode 100644 index 0000000..58233f3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -0,0 +1,393 @@ +package com.keylesspalace.tusky.repository + +import android.text.SpannedString +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.keylesspalace.tusky.db.* +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK +import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.trimTrailingWhitespace +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList + +data class Placeholder(val id: String) + +typealias TimelineStatus = Either + +enum class TimelineRequestMode { + DISK, NETWORK, ANY +} + +interface TimelineRepository { + fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, + requestMode: TimelineRequestMode): Single> + + companion object { + val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14) + } +} + +class TimelineRepositoryImpl( + private val timelineDao: TimelineDao, + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val gson: Gson +) : TimelineRepository { + + init { + this.cleanup() + } + + override fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, + limit: Int, requestMode: TimelineRequestMode + ): Single> { + val acc = accountManager.activeAccount ?: throw IllegalStateException() + val accountId = acc.id + + return if (requestMode == DISK) { + this.getStatusesFromDb(accountId, maxId, sinceId, limit) + } else { + getStatusesFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + } + } + + private fun getStatusesFromNetwork(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, limit: Int, + accountId: Long, requestMode: TimelineRequestMode + ): Single> { + return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) + .map { statuses -> + this.saveStatusesToDb(accountId, statuses, maxId, sinceId) + } + .flatMap { statuses -> + this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) + } + .onErrorResumeNext { error -> + if (error is IOException && requestMode != NETWORK) { + this.getStatusesFromDb(accountId, maxId, sinceId, limit) + } else { + Single.error(error) + } + } + } + + private fun addFromDbIfNeeded(accountId: Long, statuses: List>, + maxId: String?, sinceId: String?, limit: Int, + requestMode: TimelineRequestMode + ): Single>? { + return if (requestMode != NETWORK && statuses.size < 2) { + val newMaxID = if (statuses.isEmpty()) { + maxId + } else { + statuses.last { it.isRight() }.asRight().id + } + this.getStatusesFromDb(accountId, newMaxID, sinceId, limit) + .map { fromDb -> + // If it's just placeholders and less than limit (so we exhausted both + // db and server at this point) + if (fromDb.size < limit && fromDb.all { !it.isRight() }) { + statuses + } else { + statuses + fromDb + } + } + } else { + Single.just(statuses) + } + } + + private fun getStatusesFromDb(accountId: Long, maxId: String?, sinceId: String?, + limit: Int): Single> { + return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit) + .subscribeOn(Schedulers.io()) + .map { statuses -> + statuses.map { it.toStatus() } + } + } + + private fun saveStatusesToDb(accountId: Long, statuses: List, + maxId: String?, sinceId: String? + ): List> { + var placeholderToInsert: Placeholder? = null + + // Look for overlap + val resultStatuses = if (statuses.isNotEmpty() && sinceId != null) { + val indexOfSince = statuses.indexOfLast { it.id == sinceId } + if (indexOfSince == -1) { + // We didn't find the status which must be there. Add a placeholder + placeholderToInsert = Placeholder(sinceId.inc()) + statuses.mapTo(mutableListOf(), Status::lift) + .apply { + add(Either.Left(placeholderToInsert)) + } + } else { + // There was an overlap. Remove all overlapped statuses. No need for a placeholder. + statuses.mapTo(mutableListOf(), Status::lift) + .apply { + subList(indexOfSince, size).clear() + } + } + } else { + // Just a normal case. + statuses.map(Status::lift) + } + + Single.fromCallable { + + if(statuses.isNotEmpty()) { + timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id) + } + + for (status in statuses) { + timelineDao.insertInTransaction( + status.toEntity(accountId, gson), + status.account.toEntity(accountId, gson), + status.reblog?.account?.toEntity(accountId, gson) + ) + } + + placeholderToInsert?.let { + timelineDao.insertStatusIfNotThere(placeholderToInsert.toEntity(accountId)) + } + + // If we're loading in the bottom insert placeholder after every load + // (for requests on next launches) but not return it. + if (sinceId == null && statuses.isNotEmpty()) { + timelineDao.insertStatusIfNotThere( + Placeholder(statuses.last().id.dec()).toEntity(accountId)) + } + + // There may be placeholders which we thought could be from our TL but they are not + if (statuses.size > 2) { + timelineDao.removeAllPlaceholdersBetween(accountId, statuses.first().id, + statuses.last().id) + } else if (placeholderToInsert == null && maxId != null && sinceId != null) { + timelineDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId) + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + + return resultStatuses + } + + private fun cleanup() { + Schedulers.io().scheduleDirect { + val olderThan = System.currentTimeMillis() - TimelineRepository.CLEANUP_INTERVAL + timelineDao.cleanup(olderThan) + } + } + + private fun TimelineStatusWithAccount.toStatus(): TimelineStatus { + if (this.status.authorServerId == null) { + return Either.Left(Placeholder(this.status.serverId)) + } + + val attachments: ArrayList = gson.fromJson(status.attachments, + object : TypeToken>() {}.type) ?: ArrayList() + val mentions: Array = gson.fromJson(status.mentions, + Array::class.java) ?: arrayOf() + val application = gson.fromJson(status.application, Status.Application::class.java) + val emojis: List = gson.fromJson(status.emojis, + object : TypeToken>() {}.type) ?: listOf() + val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) + val pleroma = gson.fromJson(status.pleroma, Status.PleromaStatus::class.java) + + val reblog = status.reblogServerId?.let { id -> + Status( + id = id, + url = status.url, + account = account.toAccount(gson), + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + reblog = null, + content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), + createdAt = Date(status.createdAt), + emojis = emojis, + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + reblogged = status.reblogged, + favourited = status.favourited, + bookmarked = status.bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText!!, + visibility = status.visibility!!, + attachments = attachments, + mentions = mentions, + application = application, + pinned = false, + poll = poll, + card = null, + pleroma = pleroma + ) + } + val status = if (reblog != null) { + Status( + id = status.serverId, + url = null, // no url for reblogs + account = this.reblogAccount!!.toAccount(gson), + inReplyToId = null, + inReplyToAccountId = null, + reblog = reblog, + content = SpannedString(""), + createdAt = Date(status.createdAt), // lie but whatever? + emojis = listOf(), + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = "", + visibility = status.visibility!!, + attachments = ArrayList(), + mentions = arrayOf(), + application = null, + pinned = false, + poll = null, + card = null, + pleroma = null + ) + } else { + Status( + id = status.serverId, + url = status.url, + account = account.toAccount(gson), + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + reblog = null, + content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), + createdAt = Date(status.createdAt), + emojis = emojis, + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + reblogged = status.reblogged, + favourited = status.favourited, + bookmarked = status.bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText!!, + visibility = status.visibility!!, + attachments = attachments, + mentions = mentions, + application = application, + pinned = false, + poll = poll, + card = null, + pleroma = pleroma + ) + } + return Either.Right(status) + } +} + +private val emojisListTypeToken = object : TypeToken>() {} + +fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { + return TimelineAccountEntity( + serverId = id, + timelineUserId = accountId, + localUsername = localUsername, + username = username, + displayName = displayName.orEmpty(), + url = url, + avatar = avatar, + emojis = gson.toJson(emojis), + bot = bot + ) +} + +fun TimelineAccountEntity.toAccount(gson: Gson): Account { + return Account( + id = serverId, + localUsername = localUsername, + username = username, + displayName = displayName, + note = SpannedString(""), + url = url, + avatar = avatar, + header = "", + locked = false, + followingCount = 0, + followersCount = 0, + statusesCount = 0, + source = null, + bot = bot, + emojis = gson.fromJson(this.emojis, emojisListTypeToken.type), + fields = null, + moved = null + ) +} + + +fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { + return TimelineStatusEntity( + serverId = this.id, + url = null, + timelineUserId = timelineUserId, + authorServerId = null, + inReplyToId = null, + inReplyToAccountId = null, + content = null, + createdAt = 0L, + emojis = null, + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = null, + visibility = null, + attachments = null, + mentions = null, + application = null, + reblogServerId = null, + reblogAccountId = null, + poll = null, + pleroma = null + ) +} + +fun Status.toEntity(timelineUserId: Long, + gson: Gson): TimelineStatusEntity { + val actionable = actionableStatus + return TimelineStatusEntity( + serverId = this.id, + url = actionable.url!!, + timelineUserId = timelineUserId, + authorServerId = actionable.account.id, + inReplyToId = actionable.inReplyToId, + inReplyToAccountId = actionable.inReplyToAccountId, + content = actionable.content.toHtml(), + createdAt = actionable.createdAt.time, + emojis = actionable.emojis.let(gson::toJson), + reblogsCount = actionable.reblogsCount, + favouritesCount = actionable.favouritesCount, + reblogged = actionable.reblogged, + favourited = actionable.favourited, + bookmarked = actionable.bookmarked, + sensitive = actionable.sensitive, + spoilerText = actionable.spoilerText, + visibility = actionable.visibility, + attachments = actionable.attachments.let(gson::toJson), + mentions = actionable.mentions.let(gson::toJson), + application = actionable.application.let(gson::toJson), + reblogServerId = reblog?.id, + reblogAccountId = reblog?.let { this.account.id }, + poll = actionable.poll.let(gson::toJson), + pleroma = actionable.pleroma.let(gson::toJson) + ) +} + +fun Status.lift(): Either = Either.Right(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt new file mode 100644 index 0000000..cbda4b1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -0,0 +1,435 @@ +package com.keylesspalace.tusky.service + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.ClipData +import android.content.ClipDescription +import android.content.Context +import android.content.Intent +import android.os.* +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.SaveTootHelper +import dagger.android.AndroidInjection +import kotlinx.android.parcel.Parcelize +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class SendTootService : Service(), Injectable { + + @Inject + lateinit var mastodonApi: MastodonApi + @Inject + lateinit var accountManager: AccountManager + @Inject + lateinit var eventHub: EventHub + @Inject + lateinit var database: AppDatabase + @Inject + lateinit var draftHelper: DraftHelper + @Inject + lateinit var saveTootHelper: SaveTootHelper + + private val tootsToSend = ConcurrentHashMap() + private val sendCalls = ConcurrentHashMap, Call>>() + + private val timer = Timer() + + private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + + override fun onCreate() { + AndroidInjection.inject(this) + super.onCreate() + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (intent.hasExtra(KEY_CANCEL)) { + cancelSending(intent.getIntExtra(KEY_CANCEL, 0)) + return START_NOT_STICKY + } + + val postToSend : PostToSend = (intent.getParcelableExtra(KEY_TOOT) + ?: intent.getParcelableExtra(KEY_CHATMSG)) as PostToSend? + ?: throw IllegalStateException("SendTootService started without $KEY_CHATMSG or $KEY_TOOT extra") + + if (NotificationHelper.NOTIFICATION_USE_CHANNELS) { + val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW) + notificationManager.createNotificationChannel(channel) + } + + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_title)) + .setContentText(postToSend.getNotificationText()) + .setProgress(1, 0, true) + .setOngoing(true) + .setColor(ContextCompat.getColor(this, R.color.tusky_blue)) + .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId)) + + if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + startForeground(sendingNotificationId, builder.build()) + } else { + notificationManager.notify(sendingNotificationId, builder.build()) + } + + tootsToSend[sendingNotificationId] = postToSend + sendToot(sendingNotificationId--) + + return START_NOT_STICKY + } + + private fun sendToot(tootId: Int) { + + // when tootToSend == null, sending has been canceled + val postToSend = tootsToSend[tootId] ?: return + + // when account == null, user has logged out, cancel sending + val account = accountManager.getAccountById(postToSend.getAccountId()) + + if (account == null) { + tootsToSend.remove(tootId) + notificationManager.cancel(tootId) + stopSelfWhenDone() + return + } + + postToSend.incrementRetries() + + if(postToSend is TootToSend) { + val contentType : String? = if(postToSend.formattingSyntax.isNotEmpty()) postToSend.formattingSyntax else null + val preview : Boolean? = if(postToSend.preview) true else null + + val newStatus = NewStatus( + postToSend.text, + postToSend.warningText, + postToSend.inReplyToId, + postToSend.visibility, + postToSend.sensitive, + postToSend.mediaIds, + postToSend.scheduledAt, + postToSend.poll, + contentType, + preview + ) + + val sendCall = mastodonApi.createStatus( + "Bearer " + account.accessToken, + account.domain, + postToSend.idempotencyKey, + newStatus + ) + + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + + val scheduled = !postToSend.scheduledAt.isNullOrEmpty() + tootsToSend.remove(tootId) + + if (response.isSuccessful) { + // If the status was loaded from a draft, delete the draft and associated media files. + if (postToSend.savedTootUid != 0) { + saveTootHelper.deleteDraft(postToSend.savedTootUid) + } + if (postToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(postToSend.draftId) + .subscribe() + } + + when { + postToSend.preview -> response.body()?.let(::StatusPreviewEvent)?.let(eventHub::dispatch) + scheduled -> response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) + else -> response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch) + } + notificationManager.cancel(tootId) + + } else { + // the server refused to accept the toot, save toot & show error message + saveTootToDrafts(postToSend) + + val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_error_title)) + .setContentText(getString(R.string.send_toot_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) + + notificationManager.cancel(tootId) + notificationManager.notify(errorNotificationId--, builder.build()) + + } + + stopSelfWhenDone() + + } + + override fun onFailure(call: Call, t: Throwable) { + var backoff = TimeUnit.SECONDS.toMillis(postToSend.retries.toLong()) + if (backoff > MAX_RETRY_INTERVAL) { + backoff = MAX_RETRY_INTERVAL + } + + timer.schedule(object : TimerTask() { + override fun run() { + sendToot(tootId) + } + }, backoff) + } + } + + sendCalls[tootId] = Either.Left(sendCall) + sendCall.enqueue(callback) + } else if(postToSend is MessageToSend) { + val newMessage = NewChatMessage(postToSend.text, postToSend.mediaId) + + val sendCall = mastodonApi.createChatMessage( + "Bearer " + account.accessToken, + account.domain, + postToSend.chatId, + newMessage + ) + + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + tootsToSend.remove(tootId) + + if (response.isSuccessful) { + notificationManager.cancel(tootId) + + eventHub.dispatch(ChatMessageDeliveredEvent(response.body()!!)) + } else { + val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_error_title)) + .setContentText(getString(R.string.send_toot_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) + + notificationManager.cancel(tootId) + notificationManager.notify(errorNotificationId--, builder.build()) + } + + stopSelfWhenDone() + } + + override fun onFailure(call: Call, t: Throwable) { + var backoff = TimeUnit.SECONDS.toMillis(postToSend.retries.toLong()) + if (backoff > MAX_RETRY_INTERVAL) { + backoff = MAX_RETRY_INTERVAL + } + + timer.schedule(object : TimerTask() { + override fun run() { + sendToot(tootId) + } + }, backoff) + } + } + + sendCalls[tootId] = Either.Right(sendCall) + sendCall.enqueue(callback) + } + } + + private fun stopSelfWhenDone() { + + if (tootsToSend.isEmpty()) { + ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + } + } + + private fun cancelSending(tootId: Int) { + val tootToCancel = tootsToSend.remove(tootId) + if (tootToCancel != null) { + val sendCall = sendCalls.remove(tootId) + + sendCall?.let { + if(it.isLeft()) { + val sendStatusCall = it.asLeft() + sendStatusCall.cancel() + + saveTootToDrafts(tootToCancel as TootToSend) + } else { + val sendMessageCall = it.asRight() + sendMessageCall.cancel() + } + } + + val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_cancel_title)) + .setContentText(getString(R.string.send_toot_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) + + notificationManager.notify(tootId, builder.build()) + + timer.schedule(object : TimerTask() { + override fun run() { + notificationManager.cancel(tootId) + stopSelfWhenDone() + } + }, 5000) + + } + } + + private fun saveTootToDrafts(toot: TootToSend) { + + draftHelper.saveDraft( + draftId = toot.draftId, + accountId = toot.getAccountId(), + inReplyToId = toot.inReplyToId, + content = toot.text, + contentWarning = toot.warningText, + sensitive = toot.sensitive, + visibility = Status.Visibility.byString(toot.visibility), + mediaUris = toot.mediaUris, + mediaDescriptions = toot.mediaDescriptions, + poll = toot.poll, + formattingSyntax = toot.formattingSyntax, + failedToSend = true + ).subscribe() + } + + private fun cancelSendingIntent(tootId: Int): PendingIntent { + + val intent = Intent(this, SendTootService::class.java) + + intent.putExtra(KEY_CANCEL, tootId) + + return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + + companion object { + + private const val KEY_CHATMSG = "chatmsg" + private const val KEY_TOOT = "toot" + private const val KEY_CANCEL = "cancel_id" + private const val CHANNEL_ID = "send_toots" + + private val MAX_RETRY_INTERVAL = TimeUnit.MINUTES.toMillis(1) + + private var sendingNotificationId = -1 // use negative ids to not clash with other notis + private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis + + private fun Intent.forwardUriPermissions(mediaUris: List) { + if(mediaUris.isEmpty()) + return + + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val uriClip = ClipData( + ClipDescription("Toot Media", arrayOf("image/*", "video/*")), + ClipData.Item(mediaUris[0]) + ) + mediaUris.drop(1).forEach { uriClip.addItem(ClipData.Item(it)) } + + clipData = uriClip + } + + @JvmStatic + fun sendMessageIntent(context: Context, msgToSend: MessageToSend): Intent { + val intent = Intent(context, SendTootService::class.java) + intent.putExtra(KEY_CHATMSG, msgToSend) + if(msgToSend.mediaUri != null) + intent.forwardUriPermissions(listOf(msgToSend.mediaUri)) + + return intent + } + + @JvmStatic + fun sendTootIntent(context: Context, tootToSend: TootToSend): Intent { + val intent = Intent(context, SendTootService::class.java) + intent.putExtra(KEY_TOOT, tootToSend) + intent.forwardUriPermissions(tootToSend.mediaUris) + + return intent + } + + } +} + +interface PostToSend { + fun getAccountId() : Long + fun getNotificationText() : String + fun incrementRetries() +} + +@Parcelize +data class MessageToSend( + val text: String, + val mediaId: String?, + val mediaUri: String?, + private val accountId: Long, + val chatId: String, + var retries: Int +) : Parcelable, PostToSend { + override fun getAccountId(): Long { + return accountId + } + + override fun getNotificationText() : String { + return text + } + + override fun incrementRetries() { + retries++ + } +} + +@Parcelize +data class TootToSend( + val text: String, + val warningText: String, + val visibility: String, + val sensitive: Boolean, + val mediaIds: List, + val mediaUris: List, + val mediaDescriptions: List, + val scheduledAt: String?, + val inReplyToId: String?, + val poll: NewPoll?, + val replyingStatusContent: String?, + val replyingStatusAuthorUsername: String?, + val formattingSyntax: String, + val preview: Boolean, + private val accountId: Long, + val savedTootUid: Int, + val draftId: Int, + val idempotencyKey: String, + var retries: Int +) : Parcelable, PostToSend { + override fun getNotificationText() : String { + return if(warningText.isBlank()) text else warningText + } + + override fun getAccountId(): Long { + return accountId + } + + override fun incrementRetries() { + retries++ + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt new file mode 100644 index 0000000..8fff6e4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt @@ -0,0 +1,46 @@ +/* Copyright 2019 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.service + +import android.content.Context +import android.content.Intent +import android.os.Build + +interface ServiceClient { + fun sendToot(tootToSend: TootToSend) + + fun sendChatMessage(msgToSend: MessageToSend) +} + +class ServiceClientImpl(private val context: Context) : ServiceClient { + private fun startService(intent: Intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + override fun sendToot(tootToSend: TootToSend) { + val intent = SendTootService.sendTootIntent(context, tootToSend) + startService(intent) + } + + override fun sendChatMessage(msgToSend: MessageToSend) { + val intent = SendTootService.sendMessageIntent(context, msgToSend) + startService(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/service/StreamingService.kt b/app/src/main/java/com/keylesspalace/tusky/service/StreamingService.kt new file mode 100644 index 0000000..16d3491 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/StreamingService.kt @@ -0,0 +1,239 @@ +package com.keylesspalace.tusky.service + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import com.google.gson.Gson +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.ChatMessageReceivedEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.StreamEvent +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isLessThan +import dagger.android.AndroidInjection +import okhttp3.* +import javax.inject.Inject + +class StreamingService: Service(), Injectable { + @Inject + lateinit var api: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var gson: Gson + + @Inject + lateinit var client: OkHttpClient + + private val sockets: MutableMap = mutableMapOf() + + private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onCreate() { + AndroidInjection.inject(this) + super.onCreate() + } + + private fun stopStreamingForId(id: Long) { + if(id in sockets) { + sockets[id]!!.close(1000, null) + sockets.remove(id) + } + } + + private fun stopStreaming() { + for(sock in sockets) { + sock.value.close(1000, null) + } + sockets.clear() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + } + + notificationManager.cancel(1337) + + synchronized(serviceRunning) { + serviceRunning = false + } + } + + override fun onDestroy() { + stopStreaming() + super.onDestroy() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if(intent.getBooleanExtra(KEY_STOP_STREAMING, false)) { + Log.d(TAG, "Stream goes suya..") + stopStreaming() + stopSelfResult(startId) + return START_NOT_STICKY + } + + var description = getString(R.string.streaming_notification_description) + val accounts = accountManager.getAllAccountsOrderedByActive() + var count = 0 + for(account in accounts) { + stopStreamingForId(account.id) + + if(!account.notificationsStreamingEnabled) + continue + + val endpoint = "wss://${account.domain}/api/v1/streaming/?access_token=${account.accessToken}&stream=user:notification" + val request = Request.Builder().url(endpoint).build() + + Log.d(TAG, "Running stream for ${account.fullName}") + + sockets[account.id] = client.newWebSocket( + request, + makeStreamingListener( + "${account.fullName}/user:notification", + account + ) + ) + + description += "\n" + account.fullName + count++ + } + + if(count <= 0) { + Log.d(TAG, "No accounts. Stopping stream") + stopStreaming() + stopSelfResult(startId) + return START_NOT_STICKY + } + + if (NotificationHelper.NOTIFICATION_USE_CHANNELS) { + val channel = NotificationChannel(CHANNEL_ID, getString(R.string.streaming_notification_name), NotificationManager.IMPORTANCE_LOW) + notificationManager.createNotificationChannel(channel) + } + + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.streaming_notification_name)) + .setContentText(description) + .setOngoing(true) + .setNotificationSilent() + .setPriority(NotificationCompat.PRIORITY_MIN) + .setColor(ContextCompat.getColor(this, R.color.tusky_blue)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + startForeground(1337, builder.build()) + } else { + notificationManager.notify(1337, builder.build()) + } + + synchronized(serviceRunning) { + serviceRunning = true + } + + return START_NOT_STICKY + } + + companion object { + val CHANNEL_ID = "streaming" + val KEY_STOP_STREAMING = "stop_streaming" + val TAG = "StreamingService" + + @JvmStatic + var serviceRunning = false + + @JvmStatic + private fun startForegroundService(ctx: Context, intent: Intent) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(intent) + } else { + ctx.startService(intent) + } + } + + @JvmStatic + fun startStreaming(context: Context) { + val intent = Intent(context, StreamingService::class.java) + intent.putExtra(KEY_STOP_STREAMING, false) + + Log.d(TAG, "Starting notifications streaming service...") + + startForegroundService(context, intent) + } + + @JvmStatic + fun stopStreaming(context: Context) { + synchronized(serviceRunning) { + if(!serviceRunning) + return + + val intent = Intent(context, StreamingService::class.java) + intent.putExtra(KEY_STOP_STREAMING, true) + + Log.d(TAG, "Stopping notifications streaming service...") + + serviceRunning = false + + startForegroundService(context, intent) + } + } + } + + private fun makeStreamingListener(tag: String, account: AccountEntity) : WebSocketListener { + return object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "Stream connected to: $tag") + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG, "Stream closed for: $tag") + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.d(TAG, "Stream failed for $tag", t) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + val event = gson.fromJson(text, StreamEvent::class.java) + when(event.event) { + StreamEvent.EventType.NOTIFICATION -> { + val notification = gson.fromJson(event.payload, Notification::class.java) + NotificationHelper.make(this@StreamingService, notification, account, true) + + if(notification.type == Notification.Type.CHAT_MESSAGE) { + eventHub.dispatch(ChatMessageReceivedEvent(notification.chatMessage!!)) + } + + if(account.lastNotificationId.isLessThan(notification.id)) { + account.lastNotificationId = notification.id + accountManager.saveAccount(account) + } + } + else -> { + Log.d(TAG, "Unknown event type: ${event.event}") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt new file mode 100644 index 0000000..1e170da --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt @@ -0,0 +1,39 @@ +/* Copyright 2019 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.service + +import android.annotation.TargetApi +import android.content.Intent +import android.service.quicksettings.TileService +import com.keylesspalace.tusky.MainActivity + +/** + * Small Addition that adds in a QuickSettings tile + * opens the Compose activity or shows an account selector when multiple accounts are present + */ + +@TargetApi(24) +class TuskyTileService : TileService() { + + override fun onClick() { + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + action = Intent.ACTION_SEND + type = "text/plain" + } + startActivityAndCollapse(intent) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt new file mode 100644 index 0000000..b8ad946 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -0,0 +1,74 @@ +package com.keylesspalace.tusky.settings + +enum class AppTheme(val value: String) { + NIGHT("night"), + DAY("day"), + BLACK("black"), + AUTO("auto"), + AUTO_SYSTEM("auto_system"); + + companion object { + fun stringValues() = values().map { it.value }.toTypedArray() + } +} + +object PrefKeys { + // Note: not all of these keys are actually used as SharedPreferences keys but we must give + // each preference a key for it to work. + + const val APP_THEME = "appTheme" + const val EMOJI = "selected_emoji_font" + const val FAB_HIDE = "fabHide" + const val LANGUAGE = "language" + const val STATUS_TEXT_SIZE = "statusTextSize" + const val MAIN_NAV_POSITION = "mainNavPosition" + const val HIDE_TOP_TOOLBAR = "hideTopToolbar" + const val ABSOLUTE_TIME_VIEW = "absoluteTimeView" + const val SHOW_BOT_OVERLAY = "showBotOverlay" + const val ANIMATE_GIF_AVATARS = "animateGifAvatars" + const val USE_BLURHASH = "useBlurhash" + const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter" + const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" + const val CONFIRM_REBLOGS = "confirmReblogs" + const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" + const val BIG_EMOJIS = "bigEmojis" + const val STICKERS = "stickers" + const val ANONYMIZE_FILENAMES = "anonymizeFilenames" + const val HIDE_MUTED_USERS = "hideMutedUsers" + const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis" + const val RENDER_STATUS_AS_MENTION = "renderStatusAsMention" + + const val CUSTOM_TABS = "customTabs" + const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications" + const val WELLBEING_HIDE_STATS_POSTS = "wellbeingHideStatsPosts" + const val WELLBEING_HIDE_STATS_PROFILE = "wellbeingHideStatsProfile" + + const val HTTP_PROXY_ENABLED = "httpProxyEnabled" + const val HTTP_PROXY_SERVER = "httpProxyServer" + const val HTTP_PROXY_PORT = "httpProxyPort" + + const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy" + const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity" + const val DEFAULT_FORMATTING_SYNTAX = "defaultFormattingSyntax" + const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled" + const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia" + const val ALWAYS_OPEN_SPOILER = "alwaysOpenSpoiler" + const val LIVE_NOTIFICATIONS = "liveNotifications" + + const val NOTIFICATIONS_ENABLED = "notificationsEnabled" + const val NOTIFICATION_ALERT_LIGHT = "notificationAlertLight" + const val NOTIFICATION_ALERT_VIBRATE = "notificationAlertVibrate" + const val NOTIFICATION_ALERT_SOUND = "notificationAlertSound" + const val NOTIFICATION_FILTER_POLLS = "notificationFilterPolls" + const val NOTIFICATION_FILTER_CHAT_MESSAGES = "notificationFilterChatMessages" + const val NOTIFICATION_FILTER_FAVS = "notificationFilterFavourites" + const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" + const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" + const val NOTIFICATION_FILTER_EMOJI_REACTIONS = "notificationFilterEmojis" + const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" + const val NOTIFICATION_FILTER_MOVE = "notificationFilterMove" + const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" + + const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" + const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt new file mode 100644 index 0000000..82dfa14 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -0,0 +1,84 @@ +package com.keylesspalace.tusky.settings + +import android.content.Context +import androidx.annotation.StringRes +import androidx.preference.* +import com.keylesspalace.tusky.components.preference.EmojiPreference +import okhttp3.OkHttpClient + +class PreferenceParent( + val context: Context, + val addPref: (pref: Preference) -> Unit +) + +inline fun PreferenceParent.preference(builder: Preference.() -> Unit): Preference { + val pref = Preference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit): ListPreference { + val pref = ListPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference { + val pref = EmojiPreference(context, okHttpClient) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.switchPreference( + builder: SwitchPreference.() -> Unit +): SwitchPreference { + val pref = SwitchPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.editTextPreference( + builder: EditTextPreference.() -> Unit +): EditTextPreference { + val pref = EditTextPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.checkBoxPreference( + builder: CheckBoxPreference.() -> Unit +): CheckBoxPreference { + val pref = CheckBoxPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.preferenceCategory( + @StringRes title: Int, + builder: PreferenceParent.(PreferenceCategory) -> Unit +) { + val category = PreferenceCategory(context) + addPref(category) + category.setTitle(title) + val newParent = PreferenceParent(context) { category.addPreference(it) } + builder(newParent, category) +} + +inline fun PreferenceFragmentCompat.makePreferenceScreen( + builder: PreferenceParent.() -> Unit +): PreferenceScreen { + val context = requireContext() + val screen = preferenceManager.createPreferenceScreen(context) + val parent = PreferenceParent(context) { screen.addPreference(it) } + // For some functions (like dependencies) it's much easier for us if we attach screen first + // and change it later + preferenceScreen = screen + builder(parent) + return screen +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java b/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java new file mode 100644 index 0000000..f67e3ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java @@ -0,0 +1,83 @@ +package com.keylesspalace.tusky.util; + +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import android.text.Editable; +import android.text.Selection; +import android.text.Spannable; +import android.widget.EditText; +import me.thanel.markdownedit.SelectionUtils; + +public class BBCodeEdit { + private BBCodeEdit() { /* cannot be instantiated */ } + + public static void addBold(@NonNull Editable text) { + HTMLEdit.surroundSelectionWith(text, "[b]", "[/b]"); + } + + public static void addBold(@NonNull EditText editText) { + addBold(editText.getText()); + } + + public static void addItalic(@NonNull Editable text) { + HTMLEdit.surroundSelectionWith(text, "[i]", "[/i]"); + } + + public static void addItalic(@NonNull EditText editText) { + addItalic(editText.getText()); + } + + public static void addStrikeThrough(@NonNull Editable text) { + HTMLEdit.surroundSelectionWith(text, "[s]", "[/s]"); + } + + public static void addStrikeThrough(@NonNull EditText editText) { + addStrikeThrough(editText.getText()); + } + + public static void addLink(@NonNull Editable text) { + if (!SelectionUtils.hasSelection(text)) { + SelectionUtils.selectWordAroundCursor(text); + } + String selectedText = SelectionUtils.getSelectedText(text).toString().trim(); + + int selectionStart = SelectionUtils.getSelectionStart(text); + + String begin = "[url=url]"; + String end = "[/url]"; + String result = begin + selectedText + end; + SelectionUtils.replaceSelectedText(text, result); + + if (selectedText.length() == 0) { + Selection.setSelection(text, selectionStart + begin.length()); + } else { + selectionStart = selectionStart + 5; // [url=".length() + Selection.setSelection(text, selectionStart, selectionStart + 3); + } + } + + public static void addLink(@NonNull EditText editText) { + addLink(editText.getText()); + } + + /** + * Inserts a markdown code block to the specified EditText at the currently selected position. + * + * @param text The {@link Editable} view to which to add markdown code block. + */ + public static void addCode(@NonNull Editable text) { + HTMLEdit.surroundSelectionWith(text, "[code]", "[/code]"); + } + + /** + * Inserts a markdown code block to the specified EditText at the currently selected position. + * + * @param editText The {@link EditText} view to which to add markdown code block. + */ + public static void addCode(@NonNull EditText editText) { + addCode(editText.getText()); + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt new file mode 100644 index 0000000..dad6d55 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.keylesspalace.tusky.util + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList + +/** + * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system + */ +data class BiListing( + // the LiveData of paged lists for the UI to observe + val pagedList: LiveData>, + // represents the network request status for load data before first to show to the user + val networkStateBefore: LiveData, + // represents the network request status for load data after last to show to the user + val networkStateAfter: LiveData, + // represents the refresh status to show to the user. Separate from networkState, this + // value is importantly only when refresh is requested. + val refreshState: LiveData, + // refreshes the whole data and fetches it from scratch. + val refresh: () -> Unit, + // retries any failed requests. + val retry: () -> Unit) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt new file mode 100644 index 0000000..14aee81 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt @@ -0,0 +1,8 @@ +package com.keylesspalace.tusky.util + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class BindingViewHolder( + val binding: T +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt new file mode 100644 index 0000000..bd5f900 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt @@ -0,0 +1,130 @@ +/** + * Blurhash implementation from blurhash project: + * https://github.com/woltapp/blurhash + * Minor modifications by charlag + */ + +package com.keylesspalace.tusky.util + +import android.graphics.Bitmap +import android.graphics.Color +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +object BlurHashDecoder { + + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { + require(width > 0) { "Width must be greater than zero" } + require(height > 0) { "height must be greater than zero" } + if (blurHash == null || blurHash.length < 6) { + return null + } + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + if (blurHash.length != 4 + 2 * numCompX * numCompY) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = Array(numCompX * numCompY) { i -> + if (i == 0) { + val colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc) + } else { + val from = 4 + i * 2 + val colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch) + } + } + return composeBitmap(width, height, numCompX, numCompY, colors) + } + + private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { + var result = 0 + for (i in from until to) { + val index = charMap[str[i]] ?: -1 + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int): FloatArray { + val r = colorEnc shr 16 + val g = (colorEnc shr 8) and 255 + val b = colorEnc and 255 + return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float): FloatArray { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + return floatArrayOf( + signedPow2((r - 9) / 9.0f) * maxAc, + signedPow2((g - 9) / 9.0f) * maxAc, + signedPow2((b - 9) / 9.0f) * maxAc + ) + } + + private fun signedPow2(value: Float) = value.pow(2f).withSign(value) + + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: Array + ): Bitmap { + val imageArray = IntArray(width * height) + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis + } + } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private val charMap = listOf( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', + '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' + ) + .mapIndexed { i, c -> c to i } + .toMap() + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt new file mode 100644 index 0000000..2cf2348 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.util + +enum class CardViewMode { + NONE, + FULL_WIDTH, + INDENTED +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt b/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt new file mode 100644 index 0000000..a9e7ba8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.util + +import android.text.TextPaint +import android.text.style.ClickableSpan + +abstract class ClickableSpanNoUnderline : ClickableSpan() { + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt new file mode 100644 index 0000000..7a4ee4d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt @@ -0,0 +1,108 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.text.SpannableString +import android.text.Spanned +import android.text.TextUtils +import android.widget.MultiAutoCompleteTextView + +class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { + + private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean { + return Character.isLetterOrDigit(character) || character == '_' // simple usernames + || character == '-' // extended usernames + || character == '.' // domain dot + } + + override fun findTokenStart(text: CharSequence, cursor: Int): Int { + if (cursor == 0) { + return cursor + } + var i = cursor + var character = text[i - 1] + + // go up to first illegal character or character we're looking for (@, # or :) + while(i > 0 && !(character == '@' || character == '#' || character == ':')) { + if(!isMentionOrHashtagAllowedCharacter(character)) { + return cursor + } + + i-- + character = if (i == 0) ' ' else text[i - 1] + } + + // maybe caught domain name? try search username + if(i > 2 && character == '@') { + var j = i - 1 + var character2 = text[i - 2] + + // again go up to first illegal character or tag "@" + while(j > 0 && character2 != '@') { + if(!isMentionOrHashtagAllowedCharacter(character2)) { + break + } + + j-- + character2 = if (j == 0) ' ' else text[j - 1] + } + + // found mention symbol, override cursor + if(character2 == '@') { + i = j + character = character2 + } + } + + // Log.d("Tokenizer", "Stopped search at ${character} ${text.substring(i)}") + + if (i < 1 + || (character != '@' && character != '#' && character != ':') + || i > 1 && !Character.isWhitespace(text[i - 2])) { + return cursor + } + return i - 1 + } + + override fun findTokenEnd(text: CharSequence, cursor: Int): Int { + var i = cursor + val length = text.length + while (i < length) { + if (text[i] == ' ') { + return i + } else { + i++ + } + } + return length + } + + override fun terminateToken(text: CharSequence): CharSequence { + var i = text.length + while (i > 0 && text[i - 1] == ' ') { + i-- + } + return if (i > 0 && text[i - 1] == ' ') { + text + } else if (text is Spanned) { + val s = SpannableString(text.toString() + " ") + TextUtils.copySpansFrom(text, 0, text.length, Object::class.java, s, 0) + s + } else { + text.toString() + " " + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt new file mode 100644 index 0000000..fc84d6f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -0,0 +1,159 @@ +/* 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 . */ + +@file:JvmName("CustomEmojiHelper") +package com.keylesspalace.tusky.util + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.* +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ReplacementSpan +import android.view.View + +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition +import com.keylesspalace.tusky.entity.Emoji + +import java.lang.ref.WeakReference +import java.util.regex.Pattern +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.settings.PrefKeys + +/** + * replaces emoji shortcodes in a text with EmojiSpans + * @param text the text containing custom emojis + * @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances) + * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) + * @return the text with the shortcodes replaced by EmojiSpans +*/ +fun CharSequence.emojify(emojis: List?, view: View, forceSmallEmoji: Boolean) : CharSequence { + if(emojis.isNullOrEmpty()) + return this + + val builder = SpannableString.valueOf(this) + val pm = PreferenceManager.getDefaultSharedPreferences(view.context) + val smallEmojis = forceSmallEmoji || !pm.getBoolean(PrefKeys.BIG_EMOJIS, true) + val animate = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + + emojis.forEach { (shortcode, url) -> + val matcher = Pattern.compile(":$shortcode:", Pattern.LITERAL) + .matcher(this) + + while(matcher.find()) { + val span = if(smallEmojis) { + SmallEmojiSpan(WeakReference(view)) + } else { + EmojiSpan(WeakReference(view)) + } + + builder.setSpan(span, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + Glide.with(view) + .asDrawable() + .load(url) + .into(span.getTarget(animate)) + } + } + return builder +} + +fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { + return this.emojify(emojis, view, false) +} + +open class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() { + var imageDrawable: Drawable? = null + + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) : Int { + if (fm != null) { + /* update FontMetricsInt or otherwise span does not get drawn when + * it covers the whole text */ + val metrics = paint.fontMetricsInt + fm.top = (metrics.top * 1.3f).toInt() + fm.ascent = (metrics.ascent * 1.3f).toInt() + fm.descent = (metrics.descent * 2.0f).toInt() + fm.bottom = (metrics.bottom * 3.5f).toInt() + } + + return (paint.textSize * 2.0).toInt() + } + + override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { + imageDrawable?.let { drawable -> + canvas.save() + + val emojiSize = getSize(paint, text, start, end, null) + drawable.setBounds(0, 0, emojiSize, emojiSize) + + var transY = bottom - drawable.bounds.bottom + transY -= paint.fontMetricsInt.descent / 2 + + canvas.translate(x, transY.toFloat()) + drawable.draw(canvas) + canvas.restore() + } + } + + fun getTarget(animate : Boolean): Target { + return object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + viewWeakReference.get()?.let { view -> + if(animate && resource is Animatable) { + val callback = resource.callback + + resource.callback = object: Drawable.Callback { + override fun unscheduleDrawable(p0: Drawable, p1: Runnable) { + callback?.unscheduleDrawable(p0, p1) + } + override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) { + callback?.scheduleDrawable(p0, p1, p2) + } + override fun invalidateDrawable(p0: Drawable) { + callback?.invalidateDrawable(p0) + view.invalidate() + } + } + resource.start() + } + + imageDrawable = resource + view.invalidate() + } + } + + override fun onLoadCleared(placeholder: Drawable?) {} + } + } +} + +class SmallEmojiSpan(viewWeakReference: WeakReference) + : EmojiSpan(viewWeakReference) { + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { + if (fm != null) { + /* update FontMetricsInt or otherwise span does not get drawn when + * it covers the whole text */ + val metrics = paint.fontMetricsInt + fm.top = metrics.top + fm.ascent = metrics.ascent + fm.descent = metrics.descent + fm.bottom = metrics.bottom + } + + return paint.textSize.toInt() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt new file mode 100644 index 0000000..bda2061 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 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.util + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter + +abstract class CustomFragmentStateAdapter( + private val activity: FragmentActivity +): FragmentStateAdapter(activity) { + + fun getFragment(position: Int): Fragment? + = activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position)) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java new file mode 100644 index 0000000..e772162 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java @@ -0,0 +1,41 @@ +package com.keylesspalace.tusky.util; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextPaint; +import android.text.style.URLSpan; +import android.view.View; + +public class CustomURLSpan extends URLSpan { + public CustomURLSpan(String url) { + super(url); + } + + private CustomURLSpan(Parcel src) { + super(src); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + @Override + public CustomURLSpan createFromParcel(Parcel source) { + return new CustomURLSpan(source); + } + + @Override + public CustomURLSpan[] newArray(int size) { + return new CustomURLSpan[size]; + } + + }; + + @Override + public void onClick(View view) { + LinkHelper.openLink(getURL(), view.getContext()); + } + + @Override public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Either.kt b/app/src/main/java/com/keylesspalace/tusky/util/Either.kt new file mode 100644 index 0000000..f0955cf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Either.kt @@ -0,0 +1,47 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +/** + * Created by charlag on 05/11/17. + * + * Class to represent sum type/tagged union/variant/ADT e.t.c. + * It is either Left or Right. + */ +sealed class Either { + data class Left(val value: L) : Either() + data class Right(val value: R) : Either() + + fun isRight() = this is Right + + fun isLeft() = this is Left + + fun asLeftOrNull() = (this as? Left)?.value + + fun asRightOrNull() = (this as? Right)?.value + + fun asLeft(): L = (this as Left).value + + fun asRight(): R = (this as Right).value + + inline fun map(crossinline mapper: (R) -> N): Either { + return if (this.isLeft()) { + Left(this.asLeft()) + } else { + Right(mapper(this.asRight())) + } + } +} \ 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 new file mode 100644 index 0000000..68529c8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -0,0 +1,355 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.util.Log +import android.util.Pair +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.emoji.text.EmojiCompat +import androidx.emoji.bundled.BundledEmojiCompatConfig +import com.keylesspalace.tusky.R +import de.c1710.filemojicompat.FileEmojiCompatConfig +import io.reactivex.Observable +import io.reactivex.ObservableEmitter +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.internal.toLongOrDefault +import okio.Source +import okio.buffer +import okio.sink +import java.io.EOFException +import java.io.File +import java.io.FilenameFilter +import java.io.IOException +import kotlin.math.max + +/** + * This class bundles information about an emoji font as well as many convenient actions. + */ +class EmojiCompatFont( + val name: String, + private val display: String, + @StringRes val caption: Int, + @DrawableRes val img: Int, + val url: String, + // The version is stored as a String in the x.xx.xx format (to be able to compare versions) + val version: String) { + + private val versionCode = getVersionCode(version) + + // A list of all available font files and whether they are older than the current version or not + // They are ordered by their version codes in ascending order + private var existingFontFileCache: List>>? = null + + val id: Int + get() = FONTS.indexOf(this) + + fun getDisplay(context: Context): String { + return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default) + } + + /** + * This method will return the actual font file (regardless of its existence) for + * the current version (not necessarily the latest!). + * + * @return The font (TTF) file or null if called on SYSTEM_FONT + */ + private fun getFontFile(context: Context): File? { + return if (this !== SYSTEM_DEFAULT) { + val directory = File(context.getExternalFilesDir(null), DIRECTORY) + File(directory, "$name$version.ttf") + } else { + null + } + } + + fun getConfig(context: Context): EmojiCompat.Config { + if(this === SYSTEM_DEFAULT) + return BundledEmojiCompatConfig(context); + return FileEmojiCompatConfig(context, getLatestFontFile(context)) + } + + fun isDownloaded(context: Context): Boolean { + return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context) + } + + /** + * Checks whether there is already a font version that satisfies the current version, i.e. it + * has a higher or equal version code. + * + * @param context The Context + * @return Whether there is a font file with a higher or equal version code to the current + */ + private fun fontFileExists(context: Context): Boolean { + val existingFontFiles = getExistingFontFiles(context) + return if (existingFontFiles.isNotEmpty()) { + compareVersions(existingFontFiles.last().second, versionCode) >= 0 + } else { + false + } + } + + /** + * Deletes any older version of a font + * + * @param context The current Context + */ + private fun deleteOldVersions(context: Context) { + val existingFontFiles = getExistingFontFiles(context) + Log.d(TAG, "deleting old versions...") + Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size)) + for (fileExists in existingFontFiles) { + if (compareVersions(fileExists.second, versionCode) < 0) { + val file = fileExists.first + // Uses side effects! + Log.d(TAG, String.format("Deleted %s successfully: %s", file.absolutePath, + file.delete())) + } + } + } + + /** + * Loads all font files that are inside the files directory into an ArrayList with the information + * on whether they are older than the currently available version or not. + * + * @param context The Context + */ + private fun getExistingFontFiles(context: Context): List>> { + // Only load it once + existingFontFileCache?.let { + return it + } + // If we call this on the system default font, just return nothing... + if (this === SYSTEM_DEFAULT) { + existingFontFileCache = emptyList() + return emptyList() + } + + val directory = File(context.getExternalFilesDir(null), DIRECTORY) + // It will search for old versions using a regex that matches the font's name plus + // (if present) a version code. No version code will be regarded as version 0. + val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern() + val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") } + val foundFontFiles = directory.listFiles(ttfFilter).orEmpty() + Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found", + foundFontFiles.size)) + + return foundFontFiles.map { file -> + val matcher = fontRegex.matcher(file.name) + val versionCode = if (matcher.matches()) { + val version = matcher.group(1) + getVersionCode(version) + } else { + listOf(0) + } + Pair(file, versionCode) + }.sortedWith( + Comparator>> { a, b -> compareVersions(a.second, b.second) } + ).also { + existingFontFileCache = it + } + } + + /** + * Returns the current or latest version of this font file (if there is any) + * + * @param context The Context + * @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font. + */ + private fun getLatestFontFile(context: Context): File? { + val current = getFontFile(context) + if (current != null && current.exists()) return current + val existingFontFiles = getExistingFontFiles(context) + return existingFontFiles.firstOrNull()?.first + } + + private fun getVersionCode(version: String?): List { + if (version == null) return listOf(0) + return version.split(".").map { + it.toIntOrNull() ?: 0 + } + } + + fun downloadFontFile(context: Context, + okHttpClient: OkHttpClient): Observable { + return Observable.create { emitter: ObservableEmitter -> + // It is possible (and very likely) that the file does not exist yet + val downloadFile = getFontFile(context)!! + if (!downloadFile.exists()) { + downloadFile.parentFile?.mkdirs() + downloadFile.createNewFile() + } + val request = Request.Builder().url(url) + .build() + + val sink = downloadFile.sink().buffer() + var source: Source? = null + try { + // Download! + val response = okHttpClient.newCall(request).execute() + + val responseBody = response.body + if (response.isSuccessful && responseBody != null) { + val size = response.length() + var progress = 0f + source = responseBody.source() + try { + while (!emitter.isDisposed) { + sink.write(source, CHUNK_SIZE) + progress += CHUNK_SIZE.toFloat() + if(size > 0) { + emitter.onNext(progress / size) + } else { + emitter.onNext(-1f) + } + } + } catch (ex: EOFException) { + /* + This means we've finished downloading the file since sink.write + will throw an EOFException when the file to be read is empty. + */ + } + } else { + Log.e(TAG, "Downloading $url failed. Status code: ${response.code}") + emitter.tryOnError(Exception()) + } + + } catch (ex: IOException) { + Log.e(TAG, "Downloading $url failed.", ex) + downloadFile.deleteIfExists() + emitter.tryOnError(ex) + } finally { + source?.close() + sink.close() + if (emitter.isDisposed) { + downloadFile.deleteIfExists() + } else { + deleteOldVersions(context) + emitter.onComplete() + } + } + + } + .subscribeOn(Schedulers.io()) + + } + + /** + * Deletes the downloaded file, if it exists. Should be called when a download gets cancelled. + */ + fun deleteDownloadedFile(context: Context) { + getFontFile(context)?.deleteIfExists() + } + + override fun toString(): String { + return display + } + + companion object { + private const val TAG = "EmojiCompatFont" + + /** + * This String represents the sub-directory the fonts are stored in. + */ + private const val DIRECTORY = "emoji" + + private const val CHUNK_SIZE = 4096L + + // The system font gets some special behavior... + private val SYSTEM_DEFAULT = EmojiCompatFont("system-default", + "System Default", + R.string.caption_systememoji, + R.drawable.ic_emoji_34dp, + "", + "0") + private 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", + "Twemoji", + R.string.caption_twemoji, + R.drawable.ic_twemoji, + "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", + "12.0.0" + ) + private val NOTOEMOJI = EmojiCompatFont("NotoEmoji", + "Noto Emoji", + R.string.caption_notoemoji, + R.drawable.ic_notoemoji, + "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", + "11.0.0" + ) + + /** + * This array stores all available EmojiCompat fonts. + * References to them can simply be saved by saving their indices + */ + val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI) + + /** + * Returns the Emoji font associated with this ID + * + * @param id the ID of this font + * @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range. + */ + fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT } + + /** + * Compares two version codes to each other + * + * @param versionA The first version + * @param versionB The second version + * @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise + */ + @VisibleForTesting + fun compareVersions(versionA: List, versionB: List): Int { + val len = max(versionB.size, versionA.size) + for (i in 0 until len) { + + val vA = versionA.getOrElse(i) { 0 } + val vB = versionB.getOrElse(i) { 0 } + + // It needs to be decided on the next level + if (vA == vB) continue + // Okay, is version B newer or version A? + return vA.compareTo(vB) + } + + // The versions are equal + return 0 + } + + /** + * This method is needed because when transparent compression is used OkHttp reports + * [ResponseBody.contentLength] as -1. We try to get the header which server sent + * us manually here. + * + * @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259) + */ + private fun Response.length(): Long { + networkResponse?.let { + val header = it.header("Content-Length") ?: return -1 + return header.toLongOrDefault(-1) + } + + // In case it's a fully cached response + return body?.contentLength() ?: -1 + } + + private fun File.deleteIfExists() { + if(exists() && !delete()) { + Log.e(TAG, "Could not delete file $this") + } + } + + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java b/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java new file mode 100644 index 0000000..61de258 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java @@ -0,0 +1,1084 @@ +package com.keylesspalace.tusky.util; + +// AUTOGENERATED +public class Emojis +{ + public static final String[][] EMOJIS = new String[][] { +{ // # group: Smileys & Emotion +"😀", // 1F600 ; fully-qualified # 😀 grinning face +"😃", // 1F603 ; fully-qualified # 😃 grinning face with big eyes +"😄", // 1F604 ; fully-qualified # 😄 grinning face with smiling eyes +"😁", // 1F601 ; fully-qualified # 😁 beaming face with smiling eyes +"😆", // 1F606 ; fully-qualified # 😆 grinning squinting face +"😅", // 1F605 ; fully-qualified # 😅 grinning face with sweat +"🤣", // 1F923 ; fully-qualified # 🤣 rolling on the floor laughing +"😂", // 1F602 ; fully-qualified # 😂 face with tears of joy +"🙂", // 1F642 ; fully-qualified # 🙂 slightly smiling face +"🙃", // 1F643 ; fully-qualified # 🙃 upside-down face +"😉", // 1F609 ; fully-qualified # 😉 winking face +"😊", // 1F60A ; fully-qualified # 😊 smiling face with smiling eyes +"😇", // 1F607 ; fully-qualified # 😇 smiling face with halo +"🥰", // 1F970 ; fully-qualified # 🥰 smiling face with hearts +"😍", // 1F60D ; fully-qualified # 😍 smiling face with heart-eyes +"🤩", // 1F929 ; fully-qualified # 🤩 star-struck +"😘", // 1F618 ; fully-qualified # 😘 face blowing a kiss +"😗", // 1F617 ; fully-qualified # 😗 kissing face +"😚", // 1F61A ; fully-qualified # 😚 kissing face with closed eyes +"😙", // 1F619 ; fully-qualified # 😙 kissing face with smiling eyes +"😋", // 1F60B ; fully-qualified # 😋 face savoring food +"😛", // 1F61B ; fully-qualified # 😛 face with tongue +"😜", // 1F61C ; fully-qualified # 😜 winking face with tongue +"🤪", // 1F92A ; fully-qualified # 🤪 zany face +"😝", // 1F61D ; fully-qualified # 😝 squinting face with tongue +"🤑", // 1F911 ; fully-qualified # 🤑 money-mouth face +"🤗", // 1F917 ; fully-qualified # 🤗 hugging face +"🤭", // 1F92D ; fully-qualified # 🤭 face with hand over mouth +"🤫", // 1F92B ; fully-qualified # 🤫 shushing face +"🤔", // 1F914 ; fully-qualified # 🤔 thinking face +"🤐", // 1F910 ; fully-qualified # 🤐 zipper-mouth face +"🤨", // 1F928 ; fully-qualified # 🤨 face with raised eyebrow +"😐", // 1F610 ; fully-qualified # 😐 neutral face +"😑", // 1F611 ; fully-qualified # 😑 expressionless face +"😶", // 1F636 ; fully-qualified # 😶 face without mouth +"😏", // 1F60F ; fully-qualified # 😏 smirking face +"😒", // 1F612 ; fully-qualified # 😒 unamused face +"🙄", // 1F644 ; fully-qualified # 🙄 face with rolling eyes +"😬", // 1F62C ; fully-qualified # 😬 grimacing face +"🤥", // 1F925 ; fully-qualified # 🤥 lying face +"😌", // 1F60C ; fully-qualified # 😌 relieved face +"😔", // 1F614 ; fully-qualified # 😔 pensive face +"😪", // 1F62A ; fully-qualified # 😪 sleepy face +"🤤", // 1F924 ; fully-qualified # 🤤 drooling face +"😴", // 1F634 ; fully-qualified # 😴 sleeping face +"😷", // 1F637 ; fully-qualified # 😷 face with medical mask +"🤒", // 1F912 ; fully-qualified # 🤒 face with thermometer +"🤕", // 1F915 ; fully-qualified # 🤕 face with head-bandage +"🤢", // 1F922 ; fully-qualified # 🤢 nauseated face +"🤮", // 1F92E ; fully-qualified # 🤮 face vomiting +"🤧", // 1F927 ; fully-qualified # 🤧 sneezing face +"🥵", // 1F975 ; fully-qualified # 🥵 hot face +"🥶", // 1F976 ; fully-qualified # 🥶 cold face +"🥴", // 1F974 ; fully-qualified # 🥴 woozy face +"😵", // 1F635 ; fully-qualified # 😵 dizzy face +"🤯", // 1F92F ; fully-qualified # 🤯 exploding head +"🤠", // 1F920 ; fully-qualified # 🤠 cowboy hat face +"🥳", // 1F973 ; fully-qualified # 🥳 partying face +"😎", // 1F60E ; fully-qualified # 😎 smiling face with sunglasses +"🤓", // 1F913 ; fully-qualified # 🤓 nerd face +"🧐", // 1F9D0 ; fully-qualified # 🧐 face with monocle +"😕", // 1F615 ; fully-qualified # 😕 confused face +"😟", // 1F61F ; fully-qualified # 😟 worried face +"🙁", // 1F641 ; fully-qualified # 🙁 slightly frowning face +"😮", // 1F62E ; fully-qualified # 😮 face with open mouth +"😯", // 1F62F ; fully-qualified # 😯 hushed face +"😲", // 1F632 ; fully-qualified # 😲 astonished face +"😳", // 1F633 ; fully-qualified # 😳 flushed face +"🥺", // 1F97A ; fully-qualified # 🥺 pleading face +"😦", // 1F626 ; fully-qualified # 😦 frowning face with open mouth +"😧", // 1F627 ; fully-qualified # 😧 anguished face +"😨", // 1F628 ; fully-qualified # 😨 fearful face +"😰", // 1F630 ; fully-qualified # 😰 anxious face with sweat +"😥", // 1F625 ; fully-qualified # 😥 sad but relieved face +"😢", // 1F622 ; fully-qualified # 😢 crying face +"😭", // 1F62D ; fully-qualified # 😭 loudly crying face +"😱", // 1F631 ; fully-qualified # 😱 face screaming in fear +"😖", // 1F616 ; fully-qualified # 😖 confounded face +"😣", // 1F623 ; fully-qualified # 😣 persevering face +"😞", // 1F61E ; fully-qualified # 😞 disappointed face +"😓", // 1F613 ; fully-qualified # 😓 downcast face with sweat +"😩", // 1F629 ; fully-qualified # 😩 weary face +"😫", // 1F62B ; fully-qualified # 😫 tired face +"🥱", // 1F971 ; fully-qualified # 🥱 yawning face +"😤", // 1F624 ; fully-qualified # 😤 face with steam from nose +"😡", // 1F621 ; fully-qualified # 😡 pouting face +"😠", // 1F620 ; fully-qualified # 😠 angry face +"🤬", // 1F92C ; fully-qualified # 🤬 face with symbols on mouth +"😈", // 1F608 ; fully-qualified # 😈 smiling face with horns +"👿", // 1F47F ; fully-qualified # 👿 angry face with horns +"💀", // 1F480 ; fully-qualified # 💀 skull +"💩", // 1F4A9 ; fully-qualified # 💩 pile of poo +"🤡", // 1F921 ; fully-qualified # 🤡 clown face +"👹", // 1F479 ; fully-qualified # 👹 ogre +"👺", // 1F47A ; fully-qualified # 👺 goblin +"👻", // 1F47B ; fully-qualified # 👻 ghost +"👽", // 1F47D ; fully-qualified # 👽 alien +"👾", // 1F47E ; fully-qualified # 👾 alien monster +"🤖", // 1F916 ; fully-qualified # 🤖 robot +"😺", // 1F63A ; fully-qualified # 😺 grinning cat +"😸", // 1F638 ; fully-qualified # 😸 grinning cat with smiling eyes +"😹", // 1F639 ; fully-qualified # 😹 cat with tears of joy +"😻", // 1F63B ; fully-qualified # 😻 smiling cat with heart-eyes +"😼", // 1F63C ; fully-qualified # 😼 cat with wry smile +"😽", // 1F63D ; fully-qualified # 😽 kissing cat +"🙀", // 1F640 ; fully-qualified # 🙀 weary cat +"😿", // 1F63F ; fully-qualified # 😿 crying cat +"😾", // 1F63E ; fully-qualified # 😾 pouting cat +"🙈", // 1F648 ; fully-qualified # 🙈 see-no-evil monkey +"🙉", // 1F649 ; fully-qualified # 🙉 hear-no-evil monkey +"🙊", // 1F64A ; fully-qualified # 🙊 speak-no-evil monkey +"💋", // 1F48B ; fully-qualified # 💋 kiss mark +"💌", // 1F48C ; fully-qualified # 💌 love letter +"💘", // 1F498 ; fully-qualified # 💘 heart with arrow +"💝", // 1F49D ; fully-qualified # 💝 heart with ribbon +"💖", // 1F496 ; fully-qualified # 💖 sparkling heart +"💗", // 1F497 ; fully-qualified # 💗 growing heart +"💓", // 1F493 ; fully-qualified # 💓 beating heart +"💞", // 1F49E ; fully-qualified # 💞 revolving hearts +"💕", // 1F495 ; fully-qualified # 💕 two hearts +"💟", // 1F49F ; fully-qualified # 💟 heart decoration +"💔", // 1F494 ; fully-qualified # 💔 broken heart +"🧡", // 1F9E1 ; fully-qualified # 🧡 orange heart +"💛", // 1F49B ; fully-qualified # 💛 yellow heart +"💚", // 1F49A ; fully-qualified # 💚 green heart +"💙", // 1F499 ; fully-qualified # 💙 blue heart +"💜", // 1F49C ; fully-qualified # 💜 purple heart +"🤎", // 1F90E ; fully-qualified # 🤎 brown heart +"🖤", // 1F5A4 ; fully-qualified # 🖤 black heart +"🤍", // 1F90D ; fully-qualified # 🤍 white heart +"💯", // 1F4AF ; fully-qualified # 💯 hundred points +"💢", // 1F4A2 ; fully-qualified # 💢 anger symbol +"💥", // 1F4A5 ; fully-qualified # 💥 collision +"💫", // 1F4AB ; fully-qualified # 💫 dizzy +"💦", // 1F4A6 ; fully-qualified # 💦 sweat droplets +"💨", // 1F4A8 ; fully-qualified # 💨 dashing away +"💣", // 1F4A3 ; fully-qualified # 💣 bomb +"💬", // 1F4AC ; fully-qualified # 💬 speech balloon +"💭", // 1F4AD ; fully-qualified # 💭 thought balloon +"💤", // 1F4A4 ; fully-qualified # 💤 zzz +}, +{ // # group: People & Body +"👋", // 1F44B ; fully-qualified # 👋 waving hand +"🤚", // 1F91A ; fully-qualified # 🤚 raised back of hand +"✋", // 270B ; fully-qualified # ✋ raised hand +"🖖", // 1F596 ; fully-qualified # 🖖 vulcan salute +"👌", // 1F44C ; fully-qualified # 👌 OK hand +"🤏", // 1F90F ; fully-qualified # 🤏 pinching hand +"🤞", // 1F91E ; fully-qualified # 🤞 crossed fingers +"🤟", // 1F91F ; fully-qualified # 🤟 love-you gesture +"🤘", // 1F918 ; fully-qualified # 🤘 sign of the horns +"🤙", // 1F919 ; fully-qualified # 🤙 call me hand +"👈", // 1F448 ; fully-qualified # 👈 backhand index pointing left +"👉", // 1F449 ; fully-qualified # 👉 backhand index pointing right +"👆", // 1F446 ; fully-qualified # 👆 backhand index pointing up +"🖕", // 1F595 ; fully-qualified # 🖕 middle finger +"👇", // 1F447 ; fully-qualified # 👇 backhand index pointing down +"👍", // 1F44D ; fully-qualified # 👍 thumbs up +"👎", // 1F44E ; fully-qualified # 👎 thumbs down +"✊", // 270A ; fully-qualified # ✊ raised fist +"👊", // 1F44A ; fully-qualified # 👊 oncoming fist +"🤛", // 1F91B ; fully-qualified # 🤛 left-facing fist +"🤜", // 1F91C ; fully-qualified # 🤜 right-facing fist +"👏", // 1F44F ; fully-qualified # 👏 clapping hands +"🙌", // 1F64C ; fully-qualified # 🙌 raising hands +"👐", // 1F450 ; fully-qualified # 👐 open hands +"🤲", // 1F932 ; fully-qualified # 🤲 palms up together +"🤝", // 1F91D ; fully-qualified # 🤝 handshake +"🙏", // 1F64F ; fully-qualified # 🙏 folded hands +"💅", // 1F485 ; fully-qualified # 💅 nail polish +"🤳", // 1F933 ; fully-qualified # 🤳 selfie +"💪", // 1F4AA ; fully-qualified # 💪 flexed biceps +"🦾", // 1F9BE ; fully-qualified # 🦾 mechanical arm +"🦿", // 1F9BF ; fully-qualified # 🦿 mechanical leg +"🦵", // 1F9B5 ; fully-qualified # 🦵 leg +"🦶", // 1F9B6 ; fully-qualified # 🦶 foot +"👂", // 1F442 ; fully-qualified # 👂 ear +"🦻", // 1F9BB ; fully-qualified # 🦻 ear with hearing aid +"👃", // 1F443 ; fully-qualified # 👃 nose +"🧠", // 1F9E0 ; fully-qualified # 🧠 brain +"🦷", // 1F9B7 ; fully-qualified # 🦷 tooth +"🦴", // 1F9B4 ; fully-qualified # 🦴 bone +"👀", // 1F440 ; fully-qualified # 👀 eyes +"👅", // 1F445 ; fully-qualified # 👅 tongue +"👄", // 1F444 ; fully-qualified # 👄 mouth +"👶", // 1F476 ; fully-qualified # 👶 baby +"🧒", // 1F9D2 ; fully-qualified # 🧒 child +"👦", // 1F466 ; fully-qualified # 👦 boy +"👧", // 1F467 ; fully-qualified # 👧 girl +"🧑", // 1F9D1 ; fully-qualified # 🧑 person +"👱", // 1F471 ; fully-qualified # 👱 person: blond hair +"👨", // 1F468 ; fully-qualified # 👨 man +"🧔", // 1F9D4 ; fully-qualified # 🧔 man: beard +"👩", // 1F469 ; fully-qualified # 👩 woman +"🧓", // 1F9D3 ; fully-qualified # 🧓 older person +"👴", // 1F474 ; fully-qualified # 👴 old man +"👵", // 1F475 ; fully-qualified # 👵 old woman +"🙍", // 1F64D ; fully-qualified # 🙍 person frowning +"🙎", // 1F64E ; fully-qualified # 🙎 person pouting +"🙅", // 1F645 ; fully-qualified # 🙅 person gesturing NO +"🙆", // 1F646 ; fully-qualified # 🙆 person gesturing OK +"💁", // 1F481 ; fully-qualified # 💁 person tipping hand +"🙋", // 1F64B ; fully-qualified # 🙋 person raising hand +"🧏", // 1F9CF ; fully-qualified # 🧏 deaf person +"🙇", // 1F647 ; fully-qualified # 🙇 person bowing +"🤦", // 1F926 ; fully-qualified # 🤦 person facepalming +"🤷", // 1F937 ; fully-qualified # 🤷 person shrugging +"👮", // 1F46E ; fully-qualified # 👮 police officer +"💂", // 1F482 ; fully-qualified # 💂 guard +"👷", // 1F477 ; fully-qualified # 👷 construction worker +"🤴", // 1F934 ; fully-qualified # 🤴 prince +"👸", // 1F478 ; fully-qualified # 👸 princess +"👳", // 1F473 ; fully-qualified # 👳 person wearing turban +"👲", // 1F472 ; fully-qualified # 👲 man with Chinese cap +"🧕", // 1F9D5 ; fully-qualified # 🧕 woman with headscarf +"🤵", // 1F935 ; fully-qualified # 🤵 man in tuxedo +"👰", // 1F470 ; fully-qualified # 👰 bride with veil +"🤰", // 1F930 ; fully-qualified # 🤰 pregnant woman +"🤱", // 1F931 ; fully-qualified # 🤱 breast-feeding +"👼", // 1F47C ; fully-qualified # 👼 baby angel +"🎅", // 1F385 ; fully-qualified # 🎅 Santa Claus +"🤶", // 1F936 ; fully-qualified # 🤶 Mrs. Claus +"🦸", // 1F9B8 ; fully-qualified # 🦸 superhero +"🦹", // 1F9B9 ; fully-qualified # 🦹 supervillain +"🧙", // 1F9D9 ; fully-qualified # 🧙 mage +"🧚", // 1F9DA ; fully-qualified # 🧚 fairy +"🧛", // 1F9DB ; fully-qualified # 🧛 vampire +"🧜", // 1F9DC ; fully-qualified # 🧜 merperson +"🧝", // 1F9DD ; fully-qualified # 🧝 elf +"🧞", // 1F9DE ; fully-qualified # 🧞 genie +"🧟", // 1F9DF ; fully-qualified # 🧟 zombie +"💆", // 1F486 ; fully-qualified # 💆 person getting massage +"💇", // 1F487 ; fully-qualified # 💇 person getting haircut +"🚶", // 1F6B6 ; fully-qualified # 🚶 person walking +"🧍", // 1F9CD ; fully-qualified # 🧍 person standing +"🧎", // 1F9CE ; fully-qualified # 🧎 person kneeling +"🏃", // 1F3C3 ; fully-qualified # 🏃 person running +"💃", // 1F483 ; fully-qualified # 💃 woman dancing +"🕺", // 1F57A ; fully-qualified # 🕺 man dancing +"👯", // 1F46F ; fully-qualified # 👯 people with bunny ears +"🧖", // 1F9D6 ; fully-qualified # 🧖 person in steamy room +"🧗", // 1F9D7 ; fully-qualified # 🧗 person climbing +"🤺", // 1F93A ; fully-qualified # 🤺 person fencing +"🏇", // 1F3C7 ; fully-qualified # 🏇 horse racing +"🏂", // 1F3C2 ; fully-qualified # 🏂 snowboarder +"🏄", // 1F3C4 ; fully-qualified # 🏄 person surfing +"🚣", // 1F6A3 ; fully-qualified # 🚣 person rowing boat +"🏊", // 1F3CA ; fully-qualified # 🏊 person swimming +"🚴", // 1F6B4 ; fully-qualified # 🚴 person biking +"🚵", // 1F6B5 ; fully-qualified # 🚵 person mountain biking +"🤸", // 1F938 ; fully-qualified # 🤸 person cartwheeling +"🤼", // 1F93C ; fully-qualified # 🤼 people wrestling +"🤽", // 1F93D ; fully-qualified # 🤽 person playing water polo +"🤾", // 1F93E ; fully-qualified # 🤾 person playing handball +"🤹", // 1F939 ; fully-qualified # 🤹 person juggling +"🧘", // 1F9D8 ; fully-qualified # 🧘 person in lotus position +"🛀", // 1F6C0 ; fully-qualified # 🛀 person taking bath +"🛌", // 1F6CC ; fully-qualified # 🛌 person in bed +"👭", // 1F46D ; fully-qualified # 👭 women holding hands +"👫", // 1F46B ; fully-qualified # 👫 woman and man holding hands +"👬", // 1F46C ; fully-qualified # 👬 men holding hands +"💏", // 1F48F ; fully-qualified # 💏 kiss +"💑", // 1F491 ; fully-qualified # 💑 couple with heart +"👪", // 1F46A ; fully-qualified # 👪 family +"👤", // 1F464 ; fully-qualified # 👤 bust in silhouette +"👥", // 1F465 ; fully-qualified # 👥 busts in silhouette +"👣", // 1F463 ; fully-qualified # 👣 footprints +}, +{ // # group: Animals & Nature +"🐵", // 1F435 ; fully-qualified # 🐵 monkey face +"🐒", // 1F412 ; fully-qualified # 🐒 monkey +"🦍", // 1F98D ; fully-qualified # 🦍 gorilla +"🦧", // 1F9A7 ; fully-qualified # 🦧 orangutan +"🐶", // 1F436 ; fully-qualified # 🐶 dog face +"🐕", // 1F415 ; fully-qualified # 🐕 dog +"🦮", // 1F9AE ; fully-qualified # 🦮 guide dog +"🐩", // 1F429 ; fully-qualified # 🐩 poodle +"🐺", // 1F43A ; fully-qualified # 🐺 wolf +"🦊", // 1F98A ; fully-qualified # 🦊 fox +"🦝", // 1F99D ; fully-qualified # 🦝 raccoon +"🐱", // 1F431 ; fully-qualified # 🐱 cat face +"🐈", // 1F408 ; fully-qualified # 🐈 cat +"🦁", // 1F981 ; fully-qualified # 🦁 lion +"🐯", // 1F42F ; fully-qualified # 🐯 tiger face +"🐅", // 1F405 ; fully-qualified # 🐅 tiger +"🐆", // 1F406 ; fully-qualified # 🐆 leopard +"🐴", // 1F434 ; fully-qualified # 🐴 horse face +"🐎", // 1F40E ; fully-qualified # 🐎 horse +"🦄", // 1F984 ; fully-qualified # 🦄 unicorn +"🦓", // 1F993 ; fully-qualified # 🦓 zebra +"🦌", // 1F98C ; fully-qualified # 🦌 deer +"🐮", // 1F42E ; fully-qualified # 🐮 cow face +"🐂", // 1F402 ; fully-qualified # 🐂 ox +"🐃", // 1F403 ; fully-qualified # 🐃 water buffalo +"🐄", // 1F404 ; fully-qualified # 🐄 cow +"🐷", // 1F437 ; fully-qualified # 🐷 pig face +"🐖", // 1F416 ; fully-qualified # 🐖 pig +"🐗", // 1F417 ; fully-qualified # 🐗 boar +"🐽", // 1F43D ; fully-qualified # 🐽 pig nose +"🐏", // 1F40F ; fully-qualified # 🐏 ram +"🐑", // 1F411 ; fully-qualified # 🐑 ewe +"🐐", // 1F410 ; fully-qualified # 🐐 goat +"🐪", // 1F42A ; fully-qualified # 🐪 camel +"🐫", // 1F42B ; fully-qualified # 🐫 two-hump camel +"🦙", // 1F999 ; fully-qualified # 🦙 llama +"🦒", // 1F992 ; fully-qualified # 🦒 giraffe +"🐘", // 1F418 ; fully-qualified # 🐘 elephant +"🦏", // 1F98F ; fully-qualified # 🦏 rhinoceros +"🦛", // 1F99B ; fully-qualified # 🦛 hippopotamus +"🐭", // 1F42D ; fully-qualified # 🐭 mouse face +"🐁", // 1F401 ; fully-qualified # 🐁 mouse +"🐀", // 1F400 ; fully-qualified # 🐀 rat +"🐹", // 1F439 ; fully-qualified # 🐹 hamster +"🐰", // 1F430 ; fully-qualified # 🐰 rabbit face +"🐇", // 1F407 ; fully-qualified # 🐇 rabbit +"🦔", // 1F994 ; fully-qualified # 🦔 hedgehog +"🦇", // 1F987 ; fully-qualified # 🦇 bat +"🐻", // 1F43B ; fully-qualified # 🐻 bear +"🐨", // 1F428 ; fully-qualified # 🐨 koala +"🐼", // 1F43C ; fully-qualified # 🐼 panda +"🦥", // 1F9A5 ; fully-qualified # 🦥 sloth +"🦦", // 1F9A6 ; fully-qualified # 🦦 otter +"🦨", // 1F9A8 ; fully-qualified # 🦨 skunk +"🦘", // 1F998 ; fully-qualified # 🦘 kangaroo +"🦡", // 1F9A1 ; fully-qualified # 🦡 badger +"🐾", // 1F43E ; fully-qualified # 🐾 paw prints +"🦃", // 1F983 ; fully-qualified # 🦃 turkey +"🐔", // 1F414 ; fully-qualified # 🐔 chicken +"🐓", // 1F413 ; fully-qualified # 🐓 rooster +"🐣", // 1F423 ; fully-qualified # 🐣 hatching chick +"🐤", // 1F424 ; fully-qualified # 🐤 baby chick +"🐥", // 1F425 ; fully-qualified # 🐥 front-facing baby chick +"🐦", // 1F426 ; fully-qualified # 🐦 bird +"🐧", // 1F427 ; fully-qualified # 🐧 penguin +"🦅", // 1F985 ; fully-qualified # 🦅 eagle +"🦆", // 1F986 ; fully-qualified # 🦆 duck +"🦢", // 1F9A2 ; fully-qualified # 🦢 swan +"🦉", // 1F989 ; fully-qualified # 🦉 owl +"🦩", // 1F9A9 ; fully-qualified # 🦩 flamingo +"🦚", // 1F99A ; fully-qualified # 🦚 peacock +"🦜", // 1F99C ; fully-qualified # 🦜 parrot +"🐸", // 1F438 ; fully-qualified # 🐸 frog +"🐊", // 1F40A ; fully-qualified # 🐊 crocodile +"🐢", // 1F422 ; fully-qualified # 🐢 turtle +"🦎", // 1F98E ; fully-qualified # 🦎 lizard +"🐍", // 1F40D ; fully-qualified # 🐍 snake +"🐲", // 1F432 ; fully-qualified # 🐲 dragon face +"🐉", // 1F409 ; fully-qualified # 🐉 dragon +"🦕", // 1F995 ; fully-qualified # 🦕 sauropod +"🦖", // 1F996 ; fully-qualified # 🦖 T-Rex +"🐳", // 1F433 ; fully-qualified # 🐳 spouting whale +"🐋", // 1F40B ; fully-qualified # 🐋 whale +"🐬", // 1F42C ; fully-qualified # 🐬 dolphin +"🐟", // 1F41F ; fully-qualified # 🐟 fish +"🐠", // 1F420 ; fully-qualified # 🐠 tropical fish +"🐡", // 1F421 ; fully-qualified # 🐡 blowfish +"🦈", // 1F988 ; fully-qualified # 🦈 shark +"🐙", // 1F419 ; fully-qualified # 🐙 octopus +"🐚", // 1F41A ; fully-qualified # 🐚 spiral shell +"🐌", // 1F40C ; fully-qualified # 🐌 snail +"🦋", // 1F98B ; fully-qualified # 🦋 butterfly +"🐛", // 1F41B ; fully-qualified # 🐛 bug +"🐜", // 1F41C ; fully-qualified # 🐜 ant +"🐝", // 1F41D ; fully-qualified # 🐝 honeybee +"🐞", // 1F41E ; fully-qualified # 🐞 lady beetle +"🦗", // 1F997 ; fully-qualified # 🦗 cricket +"🦂", // 1F982 ; fully-qualified # 🦂 scorpion +"🦟", // 1F99F ; fully-qualified # 🦟 mosquito +"🦠", // 1F9A0 ; fully-qualified # 🦠 microbe +"💐", // 1F490 ; fully-qualified # 💐 bouquet +"🌸", // 1F338 ; fully-qualified # 🌸 cherry blossom +"💮", // 1F4AE ; fully-qualified # 💮 white flower +"🌹", // 1F339 ; fully-qualified # 🌹 rose +"🥀", // 1F940 ; fully-qualified # 🥀 wilted flower +"🌺", // 1F33A ; fully-qualified # 🌺 hibiscus +"🌻", // 1F33B ; fully-qualified # 🌻 sunflower +"🌼", // 1F33C ; fully-qualified # 🌼 blossom +"🌷", // 1F337 ; fully-qualified # 🌷 tulip +"🌱", // 1F331 ; fully-qualified # 🌱 seedling +"🌲", // 1F332 ; fully-qualified # 🌲 evergreen tree +"🌳", // 1F333 ; fully-qualified # 🌳 deciduous tree +"🌴", // 1F334 ; fully-qualified # 🌴 palm tree +"🌵", // 1F335 ; fully-qualified # 🌵 cactus +"🌾", // 1F33E ; fully-qualified # 🌾 sheaf of rice +"🌿", // 1F33F ; fully-qualified # 🌿 herb +"🍀", // 1F340 ; fully-qualified # 🍀 four leaf clover +"🍁", // 1F341 ; fully-qualified # 🍁 maple leaf +"🍂", // 1F342 ; fully-qualified # 🍂 fallen leaf +"🍃", // 1F343 ; fully-qualified # 🍃 leaf fluttering in wind +}, +{ // # group: Food & Drink +"🍇", // 1F347 ; fully-qualified # 🍇 grapes +"🍈", // 1F348 ; fully-qualified # 🍈 melon +"🍉", // 1F349 ; fully-qualified # 🍉 watermelon +"🍊", // 1F34A ; fully-qualified # 🍊 tangerine +"🍋", // 1F34B ; fully-qualified # 🍋 lemon +"🍌", // 1F34C ; fully-qualified # 🍌 banana +"🍍", // 1F34D ; fully-qualified # 🍍 pineapple +"🥭", // 1F96D ; fully-qualified # 🥭 mango +"🍎", // 1F34E ; fully-qualified # 🍎 red apple +"🍏", // 1F34F ; fully-qualified # 🍏 green apple +"🍐", // 1F350 ; fully-qualified # 🍐 pear +"🍑", // 1F351 ; fully-qualified # 🍑 peach +"🍒", // 1F352 ; fully-qualified # 🍒 cherries +"🍓", // 1F353 ; fully-qualified # 🍓 strawberry +"🥝", // 1F95D ; fully-qualified # 🥝 kiwi fruit +"🍅", // 1F345 ; fully-qualified # 🍅 tomato +"🥥", // 1F965 ; fully-qualified # 🥥 coconut +"🥑", // 1F951 ; fully-qualified # 🥑 avocado +"🍆", // 1F346 ; fully-qualified # 🍆 eggplant +"🥔", // 1F954 ; fully-qualified # 🥔 potato +"🥕", // 1F955 ; fully-qualified # 🥕 carrot +"🌽", // 1F33D ; fully-qualified # 🌽 ear of corn +"🥒", // 1F952 ; fully-qualified # 🥒 cucumber +"🥬", // 1F96C ; fully-qualified # 🥬 leafy green +"🥦", // 1F966 ; fully-qualified # 🥦 broccoli +"🧄", // 1F9C4 ; fully-qualified # 🧄 garlic +"🧅", // 1F9C5 ; fully-qualified # 🧅 onion +"🍄", // 1F344 ; fully-qualified # 🍄 mushroom +"🥜", // 1F95C ; fully-qualified # 🥜 peanuts +"🌰", // 1F330 ; fully-qualified # 🌰 chestnut +"🍞", // 1F35E ; fully-qualified # 🍞 bread +"🥐", // 1F950 ; fully-qualified # 🥐 croissant +"🥖", // 1F956 ; fully-qualified # 🥖 baguette bread +"🥨", // 1F968 ; fully-qualified # 🥨 pretzel +"🥯", // 1F96F ; fully-qualified # 🥯 bagel +"🥞", // 1F95E ; fully-qualified # 🥞 pancakes +"🧇", // 1F9C7 ; fully-qualified # 🧇 waffle +"🧀", // 1F9C0 ; fully-qualified # 🧀 cheese wedge +"🍖", // 1F356 ; fully-qualified # 🍖 meat on bone +"🍗", // 1F357 ; fully-qualified # 🍗 poultry leg +"🥩", // 1F969 ; fully-qualified # 🥩 cut of meat +"🥓", // 1F953 ; fully-qualified # 🥓 bacon +"🍔", // 1F354 ; fully-qualified # 🍔 hamburger +"🍟", // 1F35F ; fully-qualified # 🍟 french fries +"🍕", // 1F355 ; fully-qualified # 🍕 pizza +"🌭", // 1F32D ; fully-qualified # 🌭 hot dog +"🥪", // 1F96A ; fully-qualified # 🥪 sandwich +"🌮", // 1F32E ; fully-qualified # 🌮 taco +"🌯", // 1F32F ; fully-qualified # 🌯 burrito +"🥙", // 1F959 ; fully-qualified # 🥙 stuffed flatbread +"🧆", // 1F9C6 ; fully-qualified # 🧆 falafel +"🥚", // 1F95A ; fully-qualified # 🥚 egg +"🍳", // 1F373 ; fully-qualified # 🍳 cooking +"🥘", // 1F958 ; fully-qualified # 🥘 shallow pan of food +"🍲", // 1F372 ; fully-qualified # 🍲 pot of food +"🥣", // 1F963 ; fully-qualified # 🥣 bowl with spoon +"🥗", // 1F957 ; fully-qualified # 🥗 green salad +"🍿", // 1F37F ; fully-qualified # 🍿 popcorn +"🧈", // 1F9C8 ; fully-qualified # 🧈 butter +"🧂", // 1F9C2 ; fully-qualified # 🧂 salt +"🥫", // 1F96B ; fully-qualified # 🥫 canned food +"🍱", // 1F371 ; fully-qualified # 🍱 bento box +"🍘", // 1F358 ; fully-qualified # 🍘 rice cracker +"🍙", // 1F359 ; fully-qualified # 🍙 rice ball +"🍚", // 1F35A ; fully-qualified # 🍚 cooked rice +"🍛", // 1F35B ; fully-qualified # 🍛 curry rice +"🍜", // 1F35C ; fully-qualified # 🍜 steaming bowl +"🍝", // 1F35D ; fully-qualified # 🍝 spaghetti +"🍠", // 1F360 ; fully-qualified # 🍠 roasted sweet potato +"🍢", // 1F362 ; fully-qualified # 🍢 oden +"🍣", // 1F363 ; fully-qualified # 🍣 sushi +"🍤", // 1F364 ; fully-qualified # 🍤 fried shrimp +"🍥", // 1F365 ; fully-qualified # 🍥 fish cake with swirl +"🥮", // 1F96E ; fully-qualified # 🥮 moon cake +"🍡", // 1F361 ; fully-qualified # 🍡 dango +"🥟", // 1F95F ; fully-qualified # 🥟 dumpling +"🥠", // 1F960 ; fully-qualified # 🥠 fortune cookie +"🥡", // 1F961 ; fully-qualified # 🥡 takeout box +"🦀", // 1F980 ; fully-qualified # 🦀 crab +"🦞", // 1F99E ; fully-qualified # 🦞 lobster +"🦐", // 1F990 ; fully-qualified # 🦐 shrimp +"🦑", // 1F991 ; fully-qualified # 🦑 squid +"🦪", // 1F9AA ; fully-qualified # 🦪 oyster +"🍦", // 1F366 ; fully-qualified # 🍦 soft ice cream +"🍧", // 1F367 ; fully-qualified # 🍧 shaved ice +"🍨", // 1F368 ; fully-qualified # 🍨 ice cream +"🍩", // 1F369 ; fully-qualified # 🍩 doughnut +"🍪", // 1F36A ; fully-qualified # 🍪 cookie +"🎂", // 1F382 ; fully-qualified # 🎂 birthday cake +"🍰", // 1F370 ; fully-qualified # 🍰 shortcake +"🧁", // 1F9C1 ; fully-qualified # 🧁 cupcake +"🥧", // 1F967 ; fully-qualified # 🥧 pie +"🍫", // 1F36B ; fully-qualified # 🍫 chocolate bar +"🍬", // 1F36C ; fully-qualified # 🍬 candy +"🍭", // 1F36D ; fully-qualified # 🍭 lollipop +"🍮", // 1F36E ; fully-qualified # 🍮 custard +"🍯", // 1F36F ; fully-qualified # 🍯 honey pot +"🍼", // 1F37C ; fully-qualified # 🍼 baby bottle +"🥛", // 1F95B ; fully-qualified # 🥛 glass of milk +"☕", // 2615 ; fully-qualified # ☕ hot beverage +"🍵", // 1F375 ; fully-qualified # 🍵 teacup without handle +"🍶", // 1F376 ; fully-qualified # 🍶 sake +"🍾", // 1F37E ; fully-qualified # 🍾 bottle with popping cork +"🍷", // 1F377 ; fully-qualified # 🍷 wine glass +"🍸", // 1F378 ; fully-qualified # 🍸 cocktail glass +"🍹", // 1F379 ; fully-qualified # 🍹 tropical drink +"🍺", // 1F37A ; fully-qualified # 🍺 beer mug +"🍻", // 1F37B ; fully-qualified # 🍻 clinking beer mugs +"🥂", // 1F942 ; fully-qualified # 🥂 clinking glasses +"🥃", // 1F943 ; fully-qualified # 🥃 tumbler glass +"🥤", // 1F964 ; fully-qualified # 🥤 cup with straw +"🧃", // 1F9C3 ; fully-qualified # 🧃 beverage box +"🧉", // 1F9C9 ; fully-qualified # 🧉 mate +"🧊", // 1F9CA ; fully-qualified # 🧊 ice cube +"🥢", // 1F962 ; fully-qualified # 🥢 chopsticks +"🍴", // 1F374 ; fully-qualified # 🍴 fork and knife +"🥄", // 1F944 ; fully-qualified # 🥄 spoon +"🔪", // 1F52A ; fully-qualified # 🔪 kitchen knife +"🏺", // 1F3FA ; fully-qualified # 🏺 amphora +}, +{ // # group: Travel & Places +"🌍", // 1F30D ; fully-qualified # 🌍 globe showing Europe-Africa +"🌎", // 1F30E ; fully-qualified # 🌎 globe showing Americas +"🌏", // 1F30F ; fully-qualified # 🌏 globe showing Asia-Australia +"🌐", // 1F310 ; fully-qualified # 🌐 globe with meridians +"🗾", // 1F5FE ; fully-qualified # 🗾 map of Japan +"🧭", // 1F9ED ; fully-qualified # 🧭 compass +"🌋", // 1F30B ; fully-qualified # 🌋 volcano +"🗻", // 1F5FB ; fully-qualified # 🗻 mount fuji +"🧱", // 1F9F1 ; fully-qualified # 🧱 brick +"🏠", // 1F3E0 ; fully-qualified # 🏠 house +"🏡", // 1F3E1 ; fully-qualified # 🏡 house with garden +"🏢", // 1F3E2 ; fully-qualified # 🏢 office building +"🏣", // 1F3E3 ; fully-qualified # 🏣 Japanese post office +"🏤", // 1F3E4 ; fully-qualified # 🏤 post office +"🏥", // 1F3E5 ; fully-qualified # 🏥 hospital +"🏦", // 1F3E6 ; fully-qualified # 🏦 bank +"🏨", // 1F3E8 ; fully-qualified # 🏨 hotel +"🏩", // 1F3E9 ; fully-qualified # 🏩 love hotel +"🏪", // 1F3EA ; fully-qualified # 🏪 convenience store +"🏫", // 1F3EB ; fully-qualified # 🏫 school +"🏬", // 1F3EC ; fully-qualified # 🏬 department store +"🏭", // 1F3ED ; fully-qualified # 🏭 factory +"🏯", // 1F3EF ; fully-qualified # 🏯 Japanese castle +"🏰", // 1F3F0 ; fully-qualified # 🏰 castle +"💒", // 1F492 ; fully-qualified # 💒 wedding +"🗼", // 1F5FC ; fully-qualified # 🗼 Tokyo tower +"🗽", // 1F5FD ; fully-qualified # 🗽 Statue of Liberty +"⛪", // 26EA ; fully-qualified # ⛪ church +"🕌", // 1F54C ; fully-qualified # 🕌 mosque +"🛕", // 1F6D5 ; fully-qualified # 🛕 hindu temple +"🕍", // 1F54D ; fully-qualified # 🕍 synagogue +"🕋", // 1F54B ; fully-qualified # 🕋 kaaba +"⛲", // 26F2 ; fully-qualified # ⛲ fountain +"⛺", // 26FA ; fully-qualified # ⛺ tent +"🌁", // 1F301 ; fully-qualified # 🌁 foggy +"🌃", // 1F303 ; fully-qualified # 🌃 night with stars +"🌄", // 1F304 ; fully-qualified # 🌄 sunrise over mountains +"🌅", // 1F305 ; fully-qualified # 🌅 sunrise +"🌆", // 1F306 ; fully-qualified # 🌆 cityscape at dusk +"🌇", // 1F307 ; fully-qualified # 🌇 sunset +"🌉", // 1F309 ; fully-qualified # 🌉 bridge at night +"🎠", // 1F3A0 ; fully-qualified # 🎠 carousel horse +"🎡", // 1F3A1 ; fully-qualified # 🎡 ferris wheel +"🎢", // 1F3A2 ; fully-qualified # 🎢 roller coaster +"💈", // 1F488 ; fully-qualified # 💈 barber pole +"🎪", // 1F3AA ; fully-qualified # 🎪 circus tent +"🚂", // 1F682 ; fully-qualified # 🚂 locomotive +"🚃", // 1F683 ; fully-qualified # 🚃 railway car +"🚄", // 1F684 ; fully-qualified # 🚄 high-speed train +"🚅", // 1F685 ; fully-qualified # 🚅 bullet train +"🚆", // 1F686 ; fully-qualified # 🚆 train +"🚇", // 1F687 ; fully-qualified # 🚇 metro +"🚈", // 1F688 ; fully-qualified # 🚈 light rail +"🚉", // 1F689 ; fully-qualified # 🚉 station +"🚊", // 1F68A ; fully-qualified # 🚊 tram +"🚝", // 1F69D ; fully-qualified # 🚝 monorail +"🚞", // 1F69E ; fully-qualified # 🚞 mountain railway +"🚋", // 1F68B ; fully-qualified # 🚋 tram car +"🚌", // 1F68C ; fully-qualified # 🚌 bus +"🚍", // 1F68D ; fully-qualified # 🚍 oncoming bus +"🚎", // 1F68E ; fully-qualified # 🚎 trolleybus +"🚐", // 1F690 ; fully-qualified # 🚐 minibus +"🚑", // 1F691 ; fully-qualified # 🚑 ambulance +"🚒", // 1F692 ; fully-qualified # 🚒 fire engine +"🚓", // 1F693 ; fully-qualified # 🚓 police car +"🚔", // 1F694 ; fully-qualified # 🚔 oncoming police car +"🚕", // 1F695 ; fully-qualified # 🚕 taxi +"🚖", // 1F696 ; fully-qualified # 🚖 oncoming taxi +"🚗", // 1F697 ; fully-qualified # 🚗 automobile +"🚘", // 1F698 ; fully-qualified # 🚘 oncoming automobile +"🚙", // 1F699 ; fully-qualified # 🚙 sport utility vehicle +"🚚", // 1F69A ; fully-qualified # 🚚 delivery truck +"🚛", // 1F69B ; fully-qualified # 🚛 articulated lorry +"🚜", // 1F69C ; fully-qualified # 🚜 tractor +"🛵", // 1F6F5 ; fully-qualified # 🛵 motor scooter +"🦽", // 1F9BD ; fully-qualified # 🦽 manual wheelchair +"🦼", // 1F9BC ; fully-qualified # 🦼 motorized wheelchair +"🛺", // 1F6FA ; fully-qualified # 🛺 auto rickshaw +"🚲", // 1F6B2 ; fully-qualified # 🚲 bicycle +"🛴", // 1F6F4 ; fully-qualified # 🛴 kick scooter +"🛹", // 1F6F9 ; fully-qualified # 🛹 skateboard +"🚏", // 1F68F ; fully-qualified # 🚏 bus stop +"⛽", // 26FD ; fully-qualified # ⛽ fuel pump +"🚨", // 1F6A8 ; fully-qualified # 🚨 police car light +"🚥", // 1F6A5 ; fully-qualified # 🚥 horizontal traffic light +"🚦", // 1F6A6 ; fully-qualified # 🚦 vertical traffic light +"🛑", // 1F6D1 ; fully-qualified # 🛑 stop sign +"🚧", // 1F6A7 ; fully-qualified # 🚧 construction +"⚓", // 2693 ; fully-qualified # ⚓ anchor +"⛵", // 26F5 ; fully-qualified # ⛵ sailboat +"🛶", // 1F6F6 ; fully-qualified # 🛶 canoe +"🚤", // 1F6A4 ; fully-qualified # 🚤 speedboat +"🚢", // 1F6A2 ; fully-qualified # 🚢 ship +"🛫", // 1F6EB ; fully-qualified # 🛫 airplane departure +"🛬", // 1F6EC ; fully-qualified # 🛬 airplane arrival +"🪂", // 1FA82 ; fully-qualified # 🪂 parachute +"💺", // 1F4BA ; fully-qualified # 💺 seat +"🚁", // 1F681 ; fully-qualified # 🚁 helicopter +"🚟", // 1F69F ; fully-qualified # 🚟 suspension railway +"🚠", // 1F6A0 ; fully-qualified # 🚠 mountain cableway +"🚡", // 1F6A1 ; fully-qualified # 🚡 aerial tramway +"🚀", // 1F680 ; fully-qualified # 🚀 rocket +"🛸", // 1F6F8 ; fully-qualified # 🛸 flying saucer +"🧳", // 1F9F3 ; fully-qualified # 🧳 luggage +"⌛", // 231B ; fully-qualified # ⌛ hourglass done +"⏳", // 23F3 ; fully-qualified # ⏳ hourglass not done +"⌚", // 231A ; fully-qualified # ⌚ watch +"⏰", // 23F0 ; fully-qualified # ⏰ alarm clock +"🕛", // 1F55B ; fully-qualified # 🕛 twelve o’clock +"🕧", // 1F567 ; fully-qualified # 🕧 twelve-thirty +"🕐", // 1F550 ; fully-qualified # 🕐 one o’clock +"🕜", // 1F55C ; fully-qualified # 🕜 one-thirty +"🕑", // 1F551 ; fully-qualified # 🕑 two o’clock +"🕝", // 1F55D ; fully-qualified # 🕝 two-thirty +"🕒", // 1F552 ; fully-qualified # 🕒 three o’clock +"🕞", // 1F55E ; fully-qualified # 🕞 three-thirty +"🕓", // 1F553 ; fully-qualified # 🕓 four o’clock +"🕟", // 1F55F ; fully-qualified # 🕟 four-thirty +"🕔", // 1F554 ; fully-qualified # 🕔 five o’clock +"🕠", // 1F560 ; fully-qualified # 🕠 five-thirty +"🕕", // 1F555 ; fully-qualified # 🕕 six o’clock +"🕡", // 1F561 ; fully-qualified # 🕡 six-thirty +"🕖", // 1F556 ; fully-qualified # 🕖 seven o’clock +"🕢", // 1F562 ; fully-qualified # 🕢 seven-thirty +"🕗", // 1F557 ; fully-qualified # 🕗 eight o’clock +"🕣", // 1F563 ; fully-qualified # 🕣 eight-thirty +"🕘", // 1F558 ; fully-qualified # 🕘 nine o’clock +"🕤", // 1F564 ; fully-qualified # 🕤 nine-thirty +"🕙", // 1F559 ; fully-qualified # 🕙 ten o’clock +"🕥", // 1F565 ; fully-qualified # 🕥 ten-thirty +"🕚", // 1F55A ; fully-qualified # 🕚 eleven o’clock +"🕦", // 1F566 ; fully-qualified # 🕦 eleven-thirty +"🌑", // 1F311 ; fully-qualified # 🌑 new moon +"🌒", // 1F312 ; fully-qualified # 🌒 waxing crescent moon +"🌓", // 1F313 ; fully-qualified # 🌓 first quarter moon +"🌔", // 1F314 ; fully-qualified # 🌔 waxing gibbous moon +"🌕", // 1F315 ; fully-qualified # 🌕 full moon +"🌖", // 1F316 ; fully-qualified # 🌖 waning gibbous moon +"🌗", // 1F317 ; fully-qualified # 🌗 last quarter moon +"🌘", // 1F318 ; fully-qualified # 🌘 waning crescent moon +"🌙", // 1F319 ; fully-qualified # 🌙 crescent moon +"🌚", // 1F31A ; fully-qualified # 🌚 new moon face +"🌛", // 1F31B ; fully-qualified # 🌛 first quarter moon face +"🌜", // 1F31C ; fully-qualified # 🌜 last quarter moon face +"🌝", // 1F31D ; fully-qualified # 🌝 full moon face +"🌞", // 1F31E ; fully-qualified # 🌞 sun with face +"🪐", // 1FA90 ; fully-qualified # 🪐 ringed planet +"⭐", // 2B50 ; fully-qualified # ⭐ star +"🌟", // 1F31F ; fully-qualified # 🌟 glowing star +"🌠", // 1F320 ; fully-qualified # 🌠 shooting star +"🌌", // 1F30C ; fully-qualified # 🌌 milky way +"⛅", // 26C5 ; fully-qualified # ⛅ sun behind cloud +"🌀", // 1F300 ; fully-qualified # 🌀 cyclone +"🌈", // 1F308 ; fully-qualified # 🌈 rainbow +"🌂", // 1F302 ; fully-qualified # 🌂 closed umbrella +"☔", // 2614 ; fully-qualified # ☔ umbrella with rain drops +"⚡", // 26A1 ; fully-qualified # ⚡ high voltage +"⛄", // 26C4 ; fully-qualified # ⛄ snowman without snow +"🔥", // 1F525 ; fully-qualified # 🔥 fire +"💧", // 1F4A7 ; fully-qualified # 💧 droplet +"🌊", // 1F30A ; fully-qualified # 🌊 water wave +}, +{ // # group: Activities +"🎃", // 1F383 ; fully-qualified # 🎃 jack-o-lantern +"🎄", // 1F384 ; fully-qualified # 🎄 Christmas tree +"🎆", // 1F386 ; fully-qualified # 🎆 fireworks +"🎇", // 1F387 ; fully-qualified # 🎇 sparkler +"🧨", // 1F9E8 ; fully-qualified # 🧨 firecracker +"✨", // 2728 ; fully-qualified # ✨ sparkles +"🎈", // 1F388 ; fully-qualified # 🎈 balloon +"🎉", // 1F389 ; fully-qualified # 🎉 party popper +"🎊", // 1F38A ; fully-qualified # 🎊 confetti ball +"🎋", // 1F38B ; fully-qualified # 🎋 tanabata tree +"🎍", // 1F38D ; fully-qualified # 🎍 pine decoration +"🎎", // 1F38E ; fully-qualified # 🎎 Japanese dolls +"🎏", // 1F38F ; fully-qualified # 🎏 carp streamer +"🎐", // 1F390 ; fully-qualified # 🎐 wind chime +"🎑", // 1F391 ; fully-qualified # 🎑 moon viewing ceremony +"🧧", // 1F9E7 ; fully-qualified # 🧧 red envelope +"🎀", // 1F380 ; fully-qualified # 🎀 ribbon +"🎁", // 1F381 ; fully-qualified # 🎁 wrapped gift +"🎫", // 1F3AB ; fully-qualified # 🎫 ticket +"🏆", // 1F3C6 ; fully-qualified # 🏆 trophy +"🏅", // 1F3C5 ; fully-qualified # 🏅 sports medal +"🥇", // 1F947 ; fully-qualified # 🥇 1st place medal +"🥈", // 1F948 ; fully-qualified # 🥈 2nd place medal +"🥉", // 1F949 ; fully-qualified # 🥉 3rd place medal +"⚽", // 26BD ; fully-qualified # ⚽ soccer ball +"⚾", // 26BE ; fully-qualified # ⚾ baseball +"🥎", // 1F94E ; fully-qualified # 🥎 softball +"🏀", // 1F3C0 ; fully-qualified # 🏀 basketball +"🏐", // 1F3D0 ; fully-qualified # 🏐 volleyball +"🏈", // 1F3C8 ; fully-qualified # 🏈 american football +"🏉", // 1F3C9 ; fully-qualified # 🏉 rugby football +"🎾", // 1F3BE ; fully-qualified # 🎾 tennis +"🥏", // 1F94F ; fully-qualified # 🥏 flying disc +"🎳", // 1F3B3 ; fully-qualified # 🎳 bowling +"🏏", // 1F3CF ; fully-qualified # 🏏 cricket game +"🏑", // 1F3D1 ; fully-qualified # 🏑 field hockey +"🏒", // 1F3D2 ; fully-qualified # 🏒 ice hockey +"🥍", // 1F94D ; fully-qualified # 🥍 lacrosse +"🏓", // 1F3D3 ; fully-qualified # 🏓 ping pong +"🏸", // 1F3F8 ; fully-qualified # 🏸 badminton +"🥊", // 1F94A ; fully-qualified # 🥊 boxing glove +"🥋", // 1F94B ; fully-qualified # 🥋 martial arts uniform +"🥅", // 1F945 ; fully-qualified # 🥅 goal net +"⛳", // 26F3 ; fully-qualified # ⛳ flag in hole +"🎣", // 1F3A3 ; fully-qualified # 🎣 fishing pole +"🤿", // 1F93F ; fully-qualified # 🤿 diving mask +"🎽", // 1F3BD ; fully-qualified # 🎽 running shirt +"🎿", // 1F3BF ; fully-qualified # 🎿 skis +"🛷", // 1F6F7 ; fully-qualified # 🛷 sled +"🥌", // 1F94C ; fully-qualified # 🥌 curling stone +"🎯", // 1F3AF ; fully-qualified # 🎯 direct hit +"🪀", // 1FA80 ; fully-qualified # 🪀 yo-yo +"🪁", // 1FA81 ; fully-qualified # 🪁 kite +"🎱", // 1F3B1 ; fully-qualified # 🎱 pool 8 ball +"🔮", // 1F52E ; fully-qualified # 🔮 crystal ball +"🧿", // 1F9FF ; fully-qualified # 🧿 nazar amulet +"🎮", // 1F3AE ; fully-qualified # 🎮 video game +"🎰", // 1F3B0 ; fully-qualified # 🎰 slot machine +"🎲", // 1F3B2 ; fully-qualified # 🎲 game die +"🧩", // 1F9E9 ; fully-qualified # 🧩 puzzle piece +"🧸", // 1F9F8 ; fully-qualified # 🧸 teddy bear +"🃏", // 1F0CF ; fully-qualified # 🃏 joker +"🀄", // 1F004 ; fully-qualified # 🀄 mahjong red dragon +"🎴", // 1F3B4 ; fully-qualified # 🎴 flower playing cards +"🎭", // 1F3AD ; fully-qualified # 🎭 performing arts +"🎨", // 1F3A8 ; fully-qualified # 🎨 artist palette +"🧵", // 1F9F5 ; fully-qualified # 🧵 thread +"🧶", // 1F9F6 ; fully-qualified # 🧶 yarn +}, +{ // # group: Objects +"👓", // 1F453 ; fully-qualified # 👓 glasses +"🥽", // 1F97D ; fully-qualified # 🥽 goggles +"🥼", // 1F97C ; fully-qualified # 🥼 lab coat +"🦺", // 1F9BA ; fully-qualified # 🦺 safety vest +"👔", // 1F454 ; fully-qualified # 👔 necktie +"👕", // 1F455 ; fully-qualified # 👕 t-shirt +"👖", // 1F456 ; fully-qualified # 👖 jeans +"🧣", // 1F9E3 ; fully-qualified # 🧣 scarf +"🧤", // 1F9E4 ; fully-qualified # 🧤 gloves +"🧥", // 1F9E5 ; fully-qualified # 🧥 coat +"🧦", // 1F9E6 ; fully-qualified # 🧦 socks +"👗", // 1F457 ; fully-qualified # 👗 dress +"👘", // 1F458 ; fully-qualified # 👘 kimono +"🥻", // 1F97B ; fully-qualified # 🥻 sari +"🩱", // 1FA71 ; fully-qualified # 🩱 one-piece swimsuit +"🩲", // 1FA72 ; fully-qualified # 🩲 swim brief +"🩳", // 1FA73 ; fully-qualified # 🩳 shorts +"👙", // 1F459 ; fully-qualified # 👙 bikini +"👚", // 1F45A ; fully-qualified # 👚 woman’s clothes +"👛", // 1F45B ; fully-qualified # 👛 purse +"👜", // 1F45C ; fully-qualified # 👜 handbag +"👝", // 1F45D ; fully-qualified # 👝 clutch bag +"🎒", // 1F392 ; fully-qualified # 🎒 backpack +"👞", // 1F45E ; fully-qualified # 👞 man’s shoe +"👟", // 1F45F ; fully-qualified # 👟 running shoe +"🥾", // 1F97E ; fully-qualified # 🥾 hiking boot +"🥿", // 1F97F ; fully-qualified # 🥿 flat shoe +"👠", // 1F460 ; fully-qualified # 👠 high-heeled shoe +"👡", // 1F461 ; fully-qualified # 👡 woman’s sandal +"🩰", // 1FA70 ; fully-qualified # 🩰 ballet shoes +"👢", // 1F462 ; fully-qualified # 👢 woman’s boot +"👑", // 1F451 ; fully-qualified # 👑 crown +"👒", // 1F452 ; fully-qualified # 👒 woman’s hat +"🎩", // 1F3A9 ; fully-qualified # 🎩 top hat +"🎓", // 1F393 ; fully-qualified # 🎓 graduation cap +"🧢", // 1F9E2 ; fully-qualified # 🧢 billed cap +"📿", // 1F4FF ; fully-qualified # 📿 prayer beads +"💄", // 1F484 ; fully-qualified # 💄 lipstick +"💍", // 1F48D ; fully-qualified # 💍 ring +"💎", // 1F48E ; fully-qualified # 💎 gem stone +"🔇", // 1F507 ; fully-qualified # 🔇 muted speaker +"🔈", // 1F508 ; fully-qualified # 🔈 speaker low volume +"🔉", // 1F509 ; fully-qualified # 🔉 speaker medium volume +"🔊", // 1F50A ; fully-qualified # 🔊 speaker high volume +"📢", // 1F4E2 ; fully-qualified # 📢 loudspeaker +"📣", // 1F4E3 ; fully-qualified # 📣 megaphone +"📯", // 1F4EF ; fully-qualified # 📯 postal horn +"🔔", // 1F514 ; fully-qualified # 🔔 bell +"🔕", // 1F515 ; fully-qualified # 🔕 bell with slash +"🎼", // 1F3BC ; fully-qualified # 🎼 musical score +"🎵", // 1F3B5 ; fully-qualified # 🎵 musical note +"🎶", // 1F3B6 ; fully-qualified # 🎶 musical notes +"🎤", // 1F3A4 ; fully-qualified # 🎤 microphone +"🎧", // 1F3A7 ; fully-qualified # 🎧 headphone +"📻", // 1F4FB ; fully-qualified # 📻 radio +"🎷", // 1F3B7 ; fully-qualified # 🎷 saxophone +"🎸", // 1F3B8 ; fully-qualified # 🎸 guitar +"🎹", // 1F3B9 ; fully-qualified # 🎹 musical keyboard +"🎺", // 1F3BA ; fully-qualified # 🎺 trumpet +"🎻", // 1F3BB ; fully-qualified # 🎻 violin +"🪕", // 1FA95 ; fully-qualified # 🪕 banjo +"🥁", // 1F941 ; fully-qualified # 🥁 drum +"📱", // 1F4F1 ; fully-qualified # 📱 mobile phone +"📲", // 1F4F2 ; fully-qualified # 📲 mobile phone with arrow +"📞", // 1F4DE ; fully-qualified # 📞 telephone receiver +"📟", // 1F4DF ; fully-qualified # 📟 pager +"📠", // 1F4E0 ; fully-qualified # 📠 fax machine +"🔋", // 1F50B ; fully-qualified # 🔋 battery +"🔌", // 1F50C ; fully-qualified # 🔌 electric plug +"💻", // 1F4BB ; fully-qualified # 💻 laptop computer +"💽", // 1F4BD ; fully-qualified # 💽 computer disk +"💾", // 1F4BE ; fully-qualified # 💾 floppy disk +"💿", // 1F4BF ; fully-qualified # 💿 optical disk +"📀", // 1F4C0 ; fully-qualified # 📀 dvd +"🧮", // 1F9EE ; fully-qualified # 🧮 abacus +"🎥", // 1F3A5 ; fully-qualified # 🎥 movie camera +"🎬", // 1F3AC ; fully-qualified # 🎬 clapper board +"📺", // 1F4FA ; fully-qualified # 📺 television +"📷", // 1F4F7 ; fully-qualified # 📷 camera +"📸", // 1F4F8 ; fully-qualified # 📸 camera with flash +"📹", // 1F4F9 ; fully-qualified # 📹 video camera +"📼", // 1F4FC ; fully-qualified # 📼 videocassette +"🔍", // 1F50D ; fully-qualified # 🔍 magnifying glass tilted left +"🔎", // 1F50E ; fully-qualified # 🔎 magnifying glass tilted right +"💡", // 1F4A1 ; fully-qualified # 💡 light bulb +"🔦", // 1F526 ; fully-qualified # 🔦 flashlight +"🏮", // 1F3EE ; fully-qualified # 🏮 red paper lantern +"🪔", // 1FA94 ; fully-qualified # 🪔 diya lamp +"📔", // 1F4D4 ; fully-qualified # 📔 notebook with decorative cover +"📕", // 1F4D5 ; fully-qualified # 📕 closed book +"📖", // 1F4D6 ; fully-qualified # 📖 open book +"📗", // 1F4D7 ; fully-qualified # 📗 green book +"📘", // 1F4D8 ; fully-qualified # 📘 blue book +"📙", // 1F4D9 ; fully-qualified # 📙 orange book +"📚", // 1F4DA ; fully-qualified # 📚 books +"📓", // 1F4D3 ; fully-qualified # 📓 notebook +"📒", // 1F4D2 ; fully-qualified # 📒 ledger +"📃", // 1F4C3 ; fully-qualified # 📃 page with curl +"📜", // 1F4DC ; fully-qualified # 📜 scroll +"📄", // 1F4C4 ; fully-qualified # 📄 page facing up +"📰", // 1F4F0 ; fully-qualified # 📰 newspaper +"📑", // 1F4D1 ; fully-qualified # 📑 bookmark tabs +"🔖", // 1F516 ; fully-qualified # 🔖 bookmark +"💰", // 1F4B0 ; fully-qualified # 💰 money bag +"💴", // 1F4B4 ; fully-qualified # 💴 yen banknote +"💵", // 1F4B5 ; fully-qualified # 💵 dollar banknote +"💶", // 1F4B6 ; fully-qualified # 💶 euro banknote +"💷", // 1F4B7 ; fully-qualified # 💷 pound banknote +"💸", // 1F4B8 ; fully-qualified # 💸 money with wings +"💳", // 1F4B3 ; fully-qualified # 💳 credit card +"🧾", // 1F9FE ; fully-qualified # 🧾 receipt +"💹", // 1F4B9 ; fully-qualified # 💹 chart increasing with yen +"💱", // 1F4B1 ; fully-qualified # 💱 currency exchange +"💲", // 1F4B2 ; fully-qualified # 💲 heavy dollar sign +"📧", // 1F4E7 ; fully-qualified # 📧 e-mail +"📨", // 1F4E8 ; fully-qualified # 📨 incoming envelope +"📩", // 1F4E9 ; fully-qualified # 📩 envelope with arrow +"📤", // 1F4E4 ; fully-qualified # 📤 outbox tray +"📥", // 1F4E5 ; fully-qualified # 📥 inbox tray +"📦", // 1F4E6 ; fully-qualified # 📦 package +"📫", // 1F4EB ; fully-qualified # 📫 closed mailbox with raised flag +"📪", // 1F4EA ; fully-qualified # 📪 closed mailbox with lowered flag +"📬", // 1F4EC ; fully-qualified # 📬 open mailbox with raised flag +"📭", // 1F4ED ; fully-qualified # 📭 open mailbox with lowered flag +"📮", // 1F4EE ; fully-qualified # 📮 postbox +"📝", // 1F4DD ; fully-qualified # 📝 memo +"💼", // 1F4BC ; fully-qualified # 💼 briefcase +"📁", // 1F4C1 ; fully-qualified # 📁 file folder +"📂", // 1F4C2 ; fully-qualified # 📂 open file folder +"📅", // 1F4C5 ; fully-qualified # 📅 calendar +"📆", // 1F4C6 ; fully-qualified # 📆 tear-off calendar +"📇", // 1F4C7 ; fully-qualified # 📇 card index +"📈", // 1F4C8 ; fully-qualified # 📈 chart increasing +"📉", // 1F4C9 ; fully-qualified # 📉 chart decreasing +"📊", // 1F4CA ; fully-qualified # 📊 bar chart +"📋", // 1F4CB ; fully-qualified # 📋 clipboard +"📌", // 1F4CC ; fully-qualified # 📌 pushpin +"📍", // 1F4CD ; fully-qualified # 📍 round pushpin +"📎", // 1F4CE ; fully-qualified # 📎 paperclip +"📏", // 1F4CF ; fully-qualified # 📏 straight ruler +"📐", // 1F4D0 ; fully-qualified # 📐 triangular ruler +"🔒", // 1F512 ; fully-qualified # 🔒 locked +"🔓", // 1F513 ; fully-qualified # 🔓 unlocked +"🔏", // 1F50F ; fully-qualified # 🔏 locked with pen +"🔐", // 1F510 ; fully-qualified # 🔐 locked with key +"🔑", // 1F511 ; fully-qualified # 🔑 key +"🔨", // 1F528 ; fully-qualified # 🔨 hammer +"🪓", // 1FA93 ; fully-qualified # 🪓 axe +"🔫", // 1F52B ; fully-qualified # 🔫 pistol +"🏹", // 1F3F9 ; fully-qualified # 🏹 bow and arrow +"🔧", // 1F527 ; fully-qualified # 🔧 wrench +"🔩", // 1F529 ; fully-qualified # 🔩 nut and bolt +"🦯", // 1F9AF ; fully-qualified # 🦯 probing cane +"🔗", // 1F517 ; fully-qualified # 🔗 link +"🧰", // 1F9F0 ; fully-qualified # 🧰 toolbox +"🧲", // 1F9F2 ; fully-qualified # 🧲 magnet +"🧪", // 1F9EA ; fully-qualified # 🧪 test tube +"🧫", // 1F9EB ; fully-qualified # 🧫 petri dish +"🧬", // 1F9EC ; fully-qualified # 🧬 dna +"🔬", // 1F52C ; fully-qualified # 🔬 microscope +"🔭", // 1F52D ; fully-qualified # 🔭 telescope +"📡", // 1F4E1 ; fully-qualified # 📡 satellite antenna +"💉", // 1F489 ; fully-qualified # 💉 syringe +"🩸", // 1FA78 ; fully-qualified # 🩸 drop of blood +"💊", // 1F48A ; fully-qualified # 💊 pill +"🩹", // 1FA79 ; fully-qualified # 🩹 adhesive bandage +"🩺", // 1FA7A ; fully-qualified # 🩺 stethoscope +"🚪", // 1F6AA ; fully-qualified # 🚪 door +"🪑", // 1FA91 ; fully-qualified # 🪑 chair +"🚽", // 1F6BD ; fully-qualified # 🚽 toilet +"🚿", // 1F6BF ; fully-qualified # 🚿 shower +"🛁", // 1F6C1 ; fully-qualified # 🛁 bathtub +"🪒", // 1FA92 ; fully-qualified # 🪒 razor +"🧴", // 1F9F4 ; fully-qualified # 🧴 lotion bottle +"🧷", // 1F9F7 ; fully-qualified # 🧷 safety pin +"🧹", // 1F9F9 ; fully-qualified # 🧹 broom +"🧺", // 1F9FA ; fully-qualified # 🧺 basket +"🧻", // 1F9FB ; fully-qualified # 🧻 roll of paper +"🧼", // 1F9FC ; fully-qualified # 🧼 soap +"🧽", // 1F9FD ; fully-qualified # 🧽 sponge +"🧯", // 1F9EF ; fully-qualified # 🧯 fire extinguisher +"🛒", // 1F6D2 ; fully-qualified # 🛒 shopping cart +"🚬", // 1F6AC ; fully-qualified # 🚬 cigarette +"🗿", // 1F5FF ; fully-qualified # 🗿 moai +}, +{ // # group: Symbols +"🏧", // 1F3E7 ; fully-qualified # 🏧 ATM sign +"🚮", // 1F6AE ; fully-qualified # 🚮 litter in bin sign +"🚰", // 1F6B0 ; fully-qualified # 🚰 potable water +"♿", // 267F ; fully-qualified # ♿ wheelchair symbol +"🚹", // 1F6B9 ; fully-qualified # 🚹 men’s room +"🚺", // 1F6BA ; fully-qualified # 🚺 women’s room +"🚻", // 1F6BB ; fully-qualified # 🚻 restroom +"🚼", // 1F6BC ; fully-qualified # 🚼 baby symbol +"🚾", // 1F6BE ; fully-qualified # 🚾 water closet +"🛂", // 1F6C2 ; fully-qualified # 🛂 passport control +"🛃", // 1F6C3 ; fully-qualified # 🛃 customs +"🛄", // 1F6C4 ; fully-qualified # 🛄 baggage claim +"🛅", // 1F6C5 ; fully-qualified # 🛅 left luggage +"🚸", // 1F6B8 ; fully-qualified # 🚸 children crossing +"⛔", // 26D4 ; fully-qualified # ⛔ no entry +"🚫", // 1F6AB ; fully-qualified # 🚫 prohibited +"🚳", // 1F6B3 ; fully-qualified # 🚳 no bicycles +"🚭", // 1F6AD ; fully-qualified # 🚭 no smoking +"🚯", // 1F6AF ; fully-qualified # 🚯 no littering +"🚱", // 1F6B1 ; fully-qualified # 🚱 non-potable water +"🚷", // 1F6B7 ; fully-qualified # 🚷 no pedestrians +"📵", // 1F4F5 ; fully-qualified # 📵 no mobile phones +"🔞", // 1F51E ; fully-qualified # 🔞 no one under eighteen +"🔃", // 1F503 ; fully-qualified # 🔃 clockwise vertical arrows +"🔄", // 1F504 ; fully-qualified # 🔄 counterclockwise arrows button +"🔙", // 1F519 ; fully-qualified # 🔙 BACK arrow +"🔚", // 1F51A ; fully-qualified # 🔚 END arrow +"🔛", // 1F51B ; fully-qualified # 🔛 ON! arrow +"🔜", // 1F51C ; fully-qualified # 🔜 SOON arrow +"🔝", // 1F51D ; fully-qualified # 🔝 TOP arrow +"🛐", // 1F6D0 ; fully-qualified # 🛐 place of worship +"🕎", // 1F54E ; fully-qualified # 🕎 menorah +"🔯", // 1F52F ; fully-qualified # 🔯 dotted six-pointed star +"♈", // 2648 ; fully-qualified # ♈ Aries +"♉", // 2649 ; fully-qualified # ♉ Taurus +"♊", // 264A ; fully-qualified # ♊ Gemini +"♋", // 264B ; fully-qualified # ♋ Cancer +"♌", // 264C ; fully-qualified # ♌ Leo +"♍", // 264D ; fully-qualified # ♍ Virgo +"♎", // 264E ; fully-qualified # ♎ Libra +"♏", // 264F ; fully-qualified # ♏ Scorpio +"♐", // 2650 ; fully-qualified # ♐ Sagittarius +"♑", // 2651 ; fully-qualified # ♑ Capricorn +"♒", // 2652 ; fully-qualified # ♒ Aquarius +"♓", // 2653 ; fully-qualified # ♓ Pisces +"⛎", // 26CE ; fully-qualified # ⛎ Ophiuchus +"🔀", // 1F500 ; fully-qualified # 🔀 shuffle tracks button +"🔁", // 1F501 ; fully-qualified # 🔁 repeat button +"🔂", // 1F502 ; fully-qualified # 🔂 repeat single button +"⏩", // 23E9 ; fully-qualified # ⏩ fast-forward button +"⏪", // 23EA ; fully-qualified # ⏪ fast reverse button +"🔼", // 1F53C ; fully-qualified # 🔼 upwards button +"⏫", // 23EB ; fully-qualified # ⏫ fast up button +"🔽", // 1F53D ; fully-qualified # 🔽 downwards button +"⏬", // 23EC ; fully-qualified # ⏬ fast down button +"🎦", // 1F3A6 ; fully-qualified # 🎦 cinema +"🔅", // 1F505 ; fully-qualified # 🔅 dim button +"🔆", // 1F506 ; fully-qualified # 🔆 bright button +"📶", // 1F4F6 ; fully-qualified # 📶 antenna bars +"📳", // 1F4F3 ; fully-qualified # 📳 vibration mode +"📴", // 1F4F4 ; fully-qualified # 📴 mobile phone off +"🔱", // 1F531 ; fully-qualified # 🔱 trident emblem +"📛", // 1F4DB ; fully-qualified # 📛 name badge +"🔰", // 1F530 ; fully-qualified # 🔰 Japanese symbol for beginner +"⭕", // 2B55 ; fully-qualified # ⭕ hollow red circle +"✅", // 2705 ; fully-qualified # ✅ check mark button +"❌", // 274C ; fully-qualified # ❌ cross mark +"❎", // 274E ; fully-qualified # ❎ cross mark button +"➕", // 2795 ; fully-qualified # ➕ plus sign +"➖", // 2796 ; fully-qualified # ➖ minus sign +"➗", // 2797 ; fully-qualified # ➗ division sign +"➰", // 27B0 ; fully-qualified # ➰ curly loop +"➿", // 27BF ; fully-qualified # ➿ double curly loop +"❓", // 2753 ; fully-qualified # ❓ question mark +"❔", // 2754 ; fully-qualified # ❔ white question mark +"❕", // 2755 ; fully-qualified # ❕ white exclamation mark +"❗", // 2757 ; fully-qualified # ❗ exclamation mark +"🔟", // 1F51F ; fully-qualified # 🔟 keycap: 10 +"🔠", // 1F520 ; fully-qualified # 🔠 input latin uppercase +"🔡", // 1F521 ; fully-qualified # 🔡 input latin lowercase +"🔢", // 1F522 ; fully-qualified # 🔢 input numbers +"🔣", // 1F523 ; fully-qualified # 🔣 input symbols +"🔤", // 1F524 ; fully-qualified # 🔤 input latin letters +"🆎", // 1F18E ; fully-qualified # 🆎 AB button (blood type) +"🆑", // 1F191 ; fully-qualified # 🆑 CL button +"🆒", // 1F192 ; fully-qualified # 🆒 COOL button +"🆓", // 1F193 ; fully-qualified # 🆓 FREE button +"🆔", // 1F194 ; fully-qualified # 🆔 ID button +"🆕", // 1F195 ; fully-qualified # 🆕 NEW button +"🆖", // 1F196 ; fully-qualified # 🆖 NG button +"🆗", // 1F197 ; fully-qualified # 🆗 OK button +"🆘", // 1F198 ; fully-qualified # 🆘 SOS button +"🆙", // 1F199 ; fully-qualified # 🆙 UP! button +"🆚", // 1F19A ; fully-qualified # 🆚 VS button +"🈁", // 1F201 ; fully-qualified # 🈁 Japanese “here” button +"🈶", // 1F236 ; fully-qualified # 🈶 Japanese “not free of charge” button +"🈯", // 1F22F ; fully-qualified # 🈯 Japanese “reserved” button +"🉐", // 1F250 ; fully-qualified # 🉐 Japanese “bargain” button +"🈹", // 1F239 ; fully-qualified # 🈹 Japanese “discount” button +"🈚", // 1F21A ; fully-qualified # 🈚 Japanese “free of charge” button +"🈲", // 1F232 ; fully-qualified # 🈲 Japanese “prohibited” button +"🉑", // 1F251 ; fully-qualified # 🉑 Japanese “acceptable” button +"🈸", // 1F238 ; fully-qualified # 🈸 Japanese “application” button +"🈴", // 1F234 ; fully-qualified # 🈴 Japanese “passing grade” button +"🈳", // 1F233 ; fully-qualified # 🈳 Japanese “vacancy” button +"🈺", // 1F23A ; fully-qualified # 🈺 Japanese “open for business” button +"🈵", // 1F235 ; fully-qualified # 🈵 Japanese “no vacancy” button +"🔴", // 1F534 ; fully-qualified # 🔴 red circle +"🟠", // 1F7E0 ; fully-qualified # 🟠 orange circle +"🟡", // 1F7E1 ; fully-qualified # 🟡 yellow circle +"🟢", // 1F7E2 ; fully-qualified # 🟢 green circle +"🔵", // 1F535 ; fully-qualified # 🔵 blue circle +"🟣", // 1F7E3 ; fully-qualified # 🟣 purple circle +"🟤", // 1F7E4 ; fully-qualified # 🟤 brown circle +"⚫", // 26AB ; fully-qualified # ⚫ black circle +"⚪", // 26AA ; fully-qualified # ⚪ white circle +"🟥", // 1F7E5 ; fully-qualified # 🟥 red square +"🟧", // 1F7E7 ; fully-qualified # 🟧 orange square +"🟨", // 1F7E8 ; fully-qualified # 🟨 yellow square +"🟩", // 1F7E9 ; fully-qualified # 🟩 green square +"🟦", // 1F7E6 ; fully-qualified # 🟦 blue square +"🟪", // 1F7EA ; fully-qualified # 🟪 purple square +"🟫", // 1F7EB ; fully-qualified # 🟫 brown square +"⬛", // 2B1B ; fully-qualified # ⬛ black large square +"⬜", // 2B1C ; fully-qualified # ⬜ white large square +"◾", // 25FE ; fully-qualified # ◾ black medium-small square +"◽", // 25FD ; fully-qualified # ◽ white medium-small square +"🔶", // 1F536 ; fully-qualified # 🔶 large orange diamond +"🔷", // 1F537 ; fully-qualified # 🔷 large blue diamond +"🔸", // 1F538 ; fully-qualified # 🔸 small orange diamond +"🔹", // 1F539 ; fully-qualified # 🔹 small blue diamond +"🔺", // 1F53A ; fully-qualified # 🔺 red triangle pointed up +"🔻", // 1F53B ; fully-qualified # 🔻 red triangle pointed down +"💠", // 1F4A0 ; fully-qualified # 💠 diamond with a dot +"🔘", // 1F518 ; fully-qualified # 🔘 radio button +"🔳", // 1F533 ; fully-qualified # 🔳 white square button +"🔲", // 1F532 ; fully-qualified # 🔲 black square button +}, +{ // # group: Flags +"🏁", // 1F3C1 ; fully-qualified # 🏁 chequered flag +"🚩", // 1F6A9 ; fully-qualified # 🚩 triangular flag +"🎌", // 1F38C ; fully-qualified # 🎌 crossed flags +"🏴", // 1F3F4 ; fully-qualified # 🏴 black flag +} + }; +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt new file mode 100644 index 0000000..6f2542b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt @@ -0,0 +1,157 @@ +/* Copyright 2018 Jochem Raat + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.graphics.Matrix + +import com.keylesspalace.tusky.entity.Attachment.Focus + +/** + * Calculates the image matrix needed to maintain the correct cropping for image views based on + * their focal point. + * + * The purpose of this class is to make sure that the focal point information on media + * attachments are honoured. This class uses the custom matrix option of android ImageView's to + * customize how the image is cropped into the view. + * + * See the explanation of focal points here: + * https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point + */ +object FocalPointUtil { + /** + * Update the given matrix for the given parameters. + * + * How it works is using the following steps: + * - First we determine if the image is too wide or too tall for the view size. If it is + * too wide, we need to crop it horizontally and scale the height to fit the view + * exactly. If it is too tall we need to crop vertically and scale the width to fit the + * view exactly. + * - Then we determine what translation is needed to get the focal point in view. We + * prefer to get the focal point at the center of the preview. However if that would + * result in some part of the preview being empty, we instead align the image so that it + * fills the view, but still the focal point is always in view. + * + * @param viewWidth The width of the imageView. + * @param viewHeight The height of the imageView + * @param imageWidth The width of the actual image + * @param imageHeight The height of the actual image + * @param focus The focal point to focus + * @param mat The matrix to update, this matrix is reset() and then updated with the new + * configuration. We reuse the old matrix to prevent unnecessary allocations. + * + * @return The matrix which correctly crops the image + */ + fun updateFocalPointMatrix(viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float, + focus: Focus, + mat: Matrix) { + // Reset the cached matrix: + mat.reset() + + // calculate scaling: + val scale = calculateScaling(viewWidth, viewHeight, imageWidth, imageHeight) + mat.preScale(scale, scale) + + // calculate offsets: + var top = 0f + var left = 0f + if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) { + top = focalOffset(viewHeight, imageHeight, scale, focalYToCoordinate(focus.y)) + } else { // horizontal crop + left = focalOffset(viewWidth, imageWidth, scale, focalXToCoordinate(focus.x)) + } + + mat.postTranslate(left, top) + } + + /** + * Calculate the scaling of the image needed to make it fill the screen. + * + * The scaling used depends on if we need a vertical of horizontal crop. + */ + fun calculateScaling(viewWidth: Float, viewHeight: Float, + imageWidth: Float, imageHeight: Float): Float { + return if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) { + viewWidth / imageWidth + } else { // horizontal crop: + viewHeight / imageHeight + } + } + + /** + * Return true if we need a vertical crop, false for a horizontal crop. + */ + fun isVerticalCrop(viewWidth: Float, viewHeight: Float, + imageWidth: Float, imageHeight: Float): Boolean { + val viewRatio = viewWidth / viewHeight + val imageRatio = imageWidth / imageHeight + + return viewRatio > imageRatio + } + + /** + * Transform the focal x component to the corresponding coordinate on the image. + * + * This means that we go from a representation where the left side of the image is -1 and + * the right side +1, to a representation with the left side being 0 and the right side + * being +1. + */ + fun focalXToCoordinate(x: Float): Float { + return (x + 1) / 2 + } + + /** + * Transform the focal y component to the corresponding coordinate on the image. + * + * This means that we go from a representation where the bottom side of the image is -1 and + * the top side +1, to a representation with the top side being 0 and the bottom side + * being +1. + */ + fun focalYToCoordinate(y: Float): Float { + return (-y + 1) / 2 + } + + /** + * Calculate the relative offset needed to focus on the focal point in one direction. + * + * This method works for both the vertical and horizontal crops. It simply calculates + * what offset to take based on the proportions between the scaled image and the view + * available. It also makes sure to always fill the bounds of the view completely with + * the image. So it won't put the very edge of the image in center, because that would + * leave part of the view empty. + */ + fun focalOffset(view: Float, image: Float, + scale: Float, focal: Float): Float { + // The fraction of the image that will be in view: + val inView = view / (scale * image) + var offset = 0f + + // These values indicate the maximum and minimum focal parameter possible while still + // keeping the entire view filled with the image: + val maxFocal = 1 - inView / 2 + val minFocal = inView / 2 + + if (focal > maxFocal) { + offset = -((2 - inView) / 2) * image * scale + view * 0.5f + } else if (focal > minFocal) { + offset = -focal * image * scale + view * 0.5f + } + + return offset + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java b/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java new file mode 100644 index 0000000..bb34425 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java @@ -0,0 +1,104 @@ +package com.keylesspalace.tusky.util; + +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import android.text.Editable; +import android.text.Selection; +import android.text.Spannable; +import android.widget.EditText; +import me.thanel.markdownedit.SelectionUtils; + +public class HTMLEdit { + private HTMLEdit() { /* cannot be instantiated */ } + + public static void addBold(@NonNull Editable text) { + surroundSelectionWith(text, "", ""); + } + + public static void addBold(@NonNull EditText editText) { + addBold(editText.getText()); + } + + public static void addItalic(@NonNull Editable text) { + surroundSelectionWith(text, "", ""); + } + + public static void addItalic(@NonNull EditText editText) { + addItalic(editText.getText()); + } + + public static void addStrikeThrough(@NonNull Editable text) { + surroundSelectionWith(text, "", ""); + } + + public static void addStrikeThrough(@NonNull EditText editText) { + addStrikeThrough(editText.getText()); + } + + public static void addLink(@NonNull Editable text) { + if (!SelectionUtils.hasSelection(text)) { + SelectionUtils.selectWordAroundCursor(text); + } + String selectedText = SelectionUtils.getSelectedText(text).toString().trim(); + + int selectionStart = SelectionUtils.getSelectionStart(text); + + String begin = ""; + String end = ""; + String result = begin + selectedText + end; + SelectionUtils.replaceSelectedText(text, result); + + if (selectedText.length() == 0) { + Selection.setSelection(text, selectionStart + begin.length()); + } else { + selectionStart = selectionStart + 9; // ", ""); + } + + /** + * Inserts a markdown code block to the specified EditText at the currently selected position. + * + * @param editText The {@link EditText} view to which to add markdown code block. + */ + public static void addCode(@NonNull EditText editText) { + addCode(editText.getText()); + } + + public static void surroundSelectionWith(@NonNull Editable text, @NonNull String surroundText, @NonNull String surroundText2) { + if (!SelectionUtils.hasSelection(text)) { + SelectionUtils.selectWordAroundCursor(text); + } + CharSequence selectedText = SelectionUtils.getSelectedText(text); + int selectionStart = SelectionUtils.getSelectionStart(text); + + selectedText = selectedText.toString().trim(); + + StringBuilder result = new StringBuilder(); + result.append(surroundText).append(selectedText).append(surroundText2); + + int charactersToGoBack = 0; + if (selectedText.length() == 0) { + charactersToGoBack = surroundText2.length(); + } + + SelectionUtils.replaceSelectedText(text, result); + Selection.setSelection(text, selectionStart + result.length() - charactersToGoBack); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java new file mode 100644 index 0000000..27f3dff --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java @@ -0,0 +1,162 @@ +/* Written in 2017 by Andrew Dawson + * + * To the extent possible under law, the author(s) have dedicated all copyright and related and + * neighboring rights to this software to the public domain worldwide. This software is distributed + * without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along with this software. + * If not, see . */ + +package com.keylesspalace.tusky.util; + +import android.net.Uri; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents one link and its parameters from the link header of an HTTP message. + * + * @see RFC5988 + */ +public class HttpHeaderLink { + private static class Parameter { + public String name; + public String value; + } + + private List parameters; + public Uri uri; + + private HttpHeaderLink(String uri) { + this.uri = Uri.parse(uri); + this.parameters = new ArrayList<>(); + } + + private static int findAny(String s, int fromIndex, char[] set) { + for (int i = fromIndex; i < s.length(); i++) { + char c = s.charAt(i); + for (char member : set) { + if (c == member) { + return i; + } + } + } + return -1; + } + + private static int findEndOfQuotedString(String line, int start) { + for (int i = start; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '\\') { + i += 1; + } else if (c == '"') { + return i; + } + } + return -1; + } + + private static class ValueResult { + String value; + int end; + + ValueResult() { + end = -1; + } + + void setValue(String value) { + value = value.trim(); + if (!value.isEmpty()) { + this.value = value; + } + } + } + + private static ValueResult parseValue(String line, int start) { + ValueResult result = new ValueResult(); + int foundIndex = findAny(line, start, new char[] {';', ',', '"'}); + if (foundIndex == -1) { + result.setValue(line.substring(start)); + return result; + } + char c = line.charAt(foundIndex); + if (c == ';' || c == ',') { + result.end = foundIndex; + result.setValue(line.substring(start, foundIndex)); + return result; + } else { + int quoteEnd = findEndOfQuotedString(line, foundIndex + 1); + if (quoteEnd == -1) { + quoteEnd = line.length(); + } + result.end = quoteEnd; + result.setValue(line.substring(foundIndex + 1, quoteEnd)); + return result; + } + } + + private static int parseParameters(String line, int start, HttpHeaderLink link) { + for (int i = start; i < line.length(); i++) { + int foundIndex = findAny(line, i, new char[] {'=', ','}); + if (foundIndex == -1) { + return -1; + } else if (line.charAt(foundIndex) == ',') { + return foundIndex; + } + Parameter parameter = new Parameter(); + parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim(); + link.parameters.add(parameter); + ValueResult result = parseValue(line, foundIndex); + parameter.value = result.value; + if (result.end == -1) { + return -1; + } else { + i = result.end; + } + } + return -1; + } + + /** + * @param line the entire link header, not including the initial "Link:" + * @return all links found in the header + */ + public static List parse(@Nullable String line) { + List linkList = new ArrayList<>(); + if (line != null) { + for (int i = 0; i < line.length(); i++) { + int uriEnd = line.indexOf('>', i); + String uri = line.substring(line.indexOf('<', i) + 1, uriEnd); + HttpHeaderLink link = new HttpHeaderLink(uri); + linkList.add(link); + int parseEnd = parseParameters(line, uriEnd, link); + if (parseEnd == -1) { + break; + } else { + i = parseEnd; + } + } + } + return linkList; + } + + /** + * @param links intended to be those returned by parse() + * @param relationType of the parameter "rel", commonly "next" or "prev" + * @return the link matching the given relation type + */ + @Nullable + public static HttpHeaderLink findByRelationType(List links, + String relationType) { + for (HttpHeaderLink link : links) { + for (Parameter parameter : link.parameters) { + if (parameter.name.equals("rel") && parameter.value.equals(relationType)) { + return link; + } + } + } + return null; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java new file mode 100644 index 0000000..7c3b68a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java @@ -0,0 +1,71 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import android.content.ContentResolver; +import android.net.Uri; +import androidx.annotation.Nullable; + +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class IOUtils { + + private static final int DEFAULT_BLOCKSIZE = 16384; + + public static void closeQuietly(@Nullable Closeable stream) { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + // intentionally unhandled + } + } + + public static boolean copyToFile(ContentResolver contentResolver, Uri uri, File file) { + InputStream from; + FileOutputStream to; + try { + from = contentResolver.openInputStream(uri); + to = new FileOutputStream(file); + } catch (FileNotFoundException e) { + return false; + } + if (from == null) { + return false; + } + byte[] chunk = new byte[DEFAULT_BLOCKSIZE]; + try { + while (true) { + int bytes = from.read(chunk, 0, chunk.length); + if (bytes < 0) { + break; + } + to.write(chunk, 0, bytes); + } + } catch (IOException e) { + return false; + } + closeQuietly(from); + closeQuietly(to); + return true; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt new file mode 100644 index 0000000..9daf16f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt @@ -0,0 +1,51 @@ +@file:JvmName("ImageLoadingHelper") + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.widget.ImageView +import androidx.annotation.Px +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.keylesspalace.tusky.R + + +private val centerCropTransformation = CenterCrop() + +fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) { + + if (url.isNullOrBlank()) { + Glide.with(imageView) + .load(R.drawable.avatar_default) + .into(imageView) + } else { + if (animate) { + Glide.with(imageView) + .load(url) + .transform( + centerCropTransformation, + RoundedCorners(radius) + ) + .placeholder(R.drawable.avatar_default) + .into(imageView) + + } else { + Glide.with(imageView) + .asBitmap() + .load(url) + .transform( + centerCropTransformation, + RoundedCorners(radius) + ) + .placeholder(R.drawable.avatar_default) + .into(imageView) + } + + } +} + +fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable { + return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f)) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java new file mode 100644 index 0000000..c05f224 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -0,0 +1,265 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.preference.PreferenceManager; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.LinkListener; + +import java.net.URI; +import java.net.URISyntaxException; + +public class LinkHelper { + public static String getDomain(String urlString) { + // sometimes URL can be null due to Pleroma bug + if(urlString == null) + return ""; + + URI uri; + try { + uri = new URI(urlString); + } catch (URISyntaxException e) { + return ""; + } + String host = uri.getHost(); + if(host == null) { + return ""; + } else if (host.startsWith("www.")) { + return host.substring(4); + } else { + return host; + } + } + + /** + * Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating + * them with callbacks to notify when they're clicked. + * + * @param view the returned text will be put in + * @param content containing text with mentions, links, or hashtags + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ + public static void setClickableText(TextView view, CharSequence content, + @Nullable Status.Mention[] mentions, final LinkListener listener) { + SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content); + URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class); + for (URLSpan span : urlSpans) { + int start = builder.getSpanStart(span); + int end = builder.getSpanEnd(span); + int flags = builder.getSpanFlags(span); + CharSequence text = builder.subSequence(start, end); + ClickableSpan customSpan = null; + + if (text.charAt(0) == '#') { + final String tag = text.subSequence(1, text.length()).toString(); + customSpan = new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { listener.onViewTag(tag); } + }; + } else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) { + String accountUsername = text.subSequence(1, text.length()).toString(); + /* There may be multiple matches for users on different instances with the same + * username. If a match has the same domain we know it's for sure the same, but if + * that can't be found then just go with whichever one matched last. */ + String id = null; + for (Status.Mention mention : mentions) { + if (mention.getLocalUsername().equalsIgnoreCase(accountUsername)) { + id = mention.getId(); + String url = mention.getUrl(); + if (url != null && url.contains(getDomain(span.getURL()))) { + break; + } + } + } + if (id != null) { + final String accountId = id; + customSpan = new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } + }; + } + } + + if (customSpan == null) { + customSpan = new CustomURLSpan(span.getURL()) { + @Override + public void onClick(View widget) { + listener.onViewUrl(getURL()); + } + }; + } + builder.removeSpan(span); + builder.setSpan(customSpan, start, end, flags); + + /* Add zero-width space after links in end of line to fix its too large hitbox. + * See also : https://github.com/tuskyapp/Tusky/issues/846 + * https://github.com/tuskyapp/Tusky/pull/916 */ + if (end >= builder.length() || + builder.subSequence(end, end + 1).toString().equals("\n")){ + builder.insert(end, "\u200B"); + } + } + + view.setText(builder); + view.setMovementMethod(LinkMovementMethod.getInstance()); + } + + /** + * Put mentions in a piece of text and makes them clickable, associating them with callbacks to + * notify when they're clicked. + * + * @param view the returned text will be put in + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ + public static void setClickableMentions( + TextView view, @Nullable Status.Mention[] mentions, final LinkListener listener) { + if (mentions == null || mentions.length == 0) { + view.setText(null); + return; + } + SpannableStringBuilder builder = new SpannableStringBuilder(); + int start = 0; + int end = 0; + int flags; + boolean firstMention = true; + for (Status.Mention mention : mentions) { + String accountUsername = mention.getLocalUsername(); + final String accountId = mention.getId(); + ClickableSpan customSpan = new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } + }; + + end += 1 + accountUsername.length(); // length of @ + username + flags = builder.getSpanFlags(customSpan); + if (firstMention) { + firstMention = false; + } else { + builder.append(" "); + start += 1; + end += 1; + } + builder.append("@"); + builder.append(accountUsername); + builder.setSpan(customSpan, start, end, flags); + builder.append("\u200B"); // same reasonning than in setClickableText + end += 1; // shift position to take the previous character into account + start = end; + } + view.setText(builder); + view.setMovementMethod(LinkMovementMethod.getInstance()); + } + + public static CharSequence createClickableText(String text, String link) { + URLSpan span = new CustomURLSpan(link); + + SpannableString clickableText = new SpannableString(text); + clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return clickableText; + } + + /** + * Opens a link, depending on the settings, either in the browser or in a custom tab + * + * @param url a string containing the url to open + * @param context context + */ + public static void openLink(String url, Context context) { + if(url == null) + return; + + Uri uri = Uri.parse(url).normalizeScheme(); + + boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("customTabs", false); + if (useCustomTabs) { + openLinkInCustomTab(uri, context); + } else { + openLinkInBrowser(uri, context); + } + } + + /** + * opens a link in the browser via Intent.ACTION_VIEW + * + * @param uri the uri to open + * @param context context + */ + public static void openLinkInBrowser(Uri uri, Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w("LinkHelper", "Actvity was not found for intent, " + intent); + } + } + + /** + * tries to open a link in a custom tab + * falls back to browser if not possible + * + * @param uri the uri to open + * @param context context + */ + public static void openLinkInCustomTab(Uri uri, Context context) { + int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface); + int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor); + int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor); + + CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build(); + + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build(); + + try { + customTabsIntent.launchUrl(context, uri); + } catch (ActivityNotFoundException e) { + Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent); + openLinkInBrowser(uri, context); + } + + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt new file mode 100644 index 0000000..8594dfc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -0,0 +1,325 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.os.Bundle +import android.text.Spannable +import android.text.style.URLSpan +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.entity.Status.Companion.MAX_MEDIA_ATTACHMENTS +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlin.math.min + +// Not using lambdas because there's boxing of int then +interface StatusProvider { + fun getStatus(pos: Int): StatusViewData? +} + +class ListStatusAccessibilityDelegate( + private val recyclerView: RecyclerView, + private val statusActionListener: StatusActionListener, + private val statusProvider: StatusProvider +) : RecyclerViewAccessibilityDelegate(recyclerView) { + private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) + as AccessibilityManager + + override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate + + private val context: Context get() = recyclerView.context + + private val itemDelegate = object : RecyclerViewAccessibilityDelegate.ItemDelegate(this) { + override fun onInitializeAccessibilityNodeInfo(host: View, + info: AccessibilityNodeInfoCompat) { + super.onInitializeAccessibilityNodeInfo(host, info) + + val pos = recyclerView.getChildAdapterPosition(host) + val status = statusProvider.getStatus(pos) ?: return + if (status is StatusViewData.Concrete) { + if (!status.spoilerText.isNullOrEmpty()) { + info.addAction(if (status.isExpanded) collapseCwAction else expandCwAction) + } + + info.addAction(replyAction) + + if (status.rebloggingEnabled) { + info.addAction(if (status.isReblogged) unreblogAction else reblogAction) + } + info.addAction(if (status.isFavourited) unfavouriteAction else favouriteAction) + info.addAction(if (status.isBookmarked) unbookmarkAction else bookmarkAction) + + val mediaActions = intArrayOf( + R.id.action_open_media_1, + R.id.action_open_media_2, + R.id.action_open_media_3, + R.id.action_open_media_4) + val attachmentCount = min(status.attachments.size, MAX_MEDIA_ATTACHMENTS) + for (i in 0 until attachmentCount) { + info.addAction(AccessibilityActionCompat( + mediaActions[i], + context.getString(R.string.action_open_media_n, i + 1))) + } + + info.addAction(openProfileAction) + if (getLinks(status).any()) info.addAction(linksAction) + + val mentions = status.mentions + if (mentions != null && mentions.isNotEmpty()) info.addAction(mentionsAction) + + if (getHashtags(status).any()) info.addAction(hashtagsAction) + if (!status.rebloggedByUsername.isNullOrEmpty()) { + info.addAction(openRebloggerAction) + } + if (status.reblogsCount > 0) info.addAction(openRebloggedByAction) + if (status.favouritesCount > 0) info.addAction(openFavsAction) + + info.addAction(moreAction) + } + + } + + override fun performAccessibilityAction(host: View, action: Int, + args: Bundle?): Boolean { + val pos = recyclerView.getChildAdapterPosition(host) + when (action) { + R.id.action_reply -> { + interrupt() + statusActionListener.onReply(pos) + } + R.id.action_favourite -> statusActionListener.onFavourite(true, pos) + R.id.action_unfavourite -> statusActionListener.onFavourite(false, pos) + R.id.action_bookmark -> statusActionListener.onBookmark(true, pos) + R.id.action_unbookmark -> statusActionListener.onBookmark(false, pos) + R.id.action_reblog -> statusActionListener.onReblog(true, pos) + R.id.action_unreblog -> statusActionListener.onReblog(false, pos) + R.id.action_open_profile -> { + interrupt() + statusActionListener.onViewAccount( + (statusProvider.getStatus(pos) as StatusViewData.Concrete).senderId) + } + R.id.action_open_media_1 -> { + interrupt() + statusActionListener.onViewMedia(pos, 0, null) + } + R.id.action_open_media_2 -> { + interrupt() + statusActionListener.onViewMedia(pos, 1, null) + } + R.id.action_open_media_3 -> { + interrupt() + statusActionListener.onViewMedia(pos, 2, null) + } + R.id.action_open_media_4 -> { + interrupt() + statusActionListener.onViewMedia(pos, 3, null) + } + R.id.action_expand_cw -> { + // Toggling it directly to avoid animations + // which cannot be disabled for detaild status for some reason + val holder = recyclerView.getChildViewHolder(host) as StatusBaseViewHolder + holder.toggleContentWarning() + // Stop and restart narrator before it reads old description. + // Would be nice if we could *just* read the content here but doesn't seem + // to be possible. + forceFocus(host) + } + R.id.action_collapse_cw -> { + statusActionListener.onExpandedChange(false, pos) + interrupt() + } + R.id.action_links -> showLinksDialog(host) + R.id.action_mentions -> showMentionsDialog(host) + R.id.action_hashtags -> showHashtagsDialog(host) + R.id.action_open_reblogger -> { + interrupt() + statusActionListener.onOpenReblog(pos) + } + R.id.action_open_reblogged_by -> { + interrupt() + statusActionListener.onShowReblogs(pos) + } + R.id.action_open_faved_by -> { + interrupt() + statusActionListener.onShowFavs(pos) + } + R.id.action_more -> { + statusActionListener.onMore(host, pos) + } + else -> return super.performAccessibilityAction(host, action, args) + } + return true + } + + + private fun showLinksDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val links = getLinks(status).toList() + val textLinks = links.map { item -> item.link } + AlertDialog.Builder(host.context) + .setTitle(R.string.title_links_dialog) + .setAdapter(ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, + textLinks) + ) { _, which -> LinkHelper.openLink(links[which].link, host.context) } + .show() + .let { forceFocus(it.listView) } + } + + private fun showMentionsDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val mentions = status.mentions ?: return + val stringMentions = mentions.map { it.username } + AlertDialog.Builder(host.context) + .setTitle(R.string.title_mentions_dialog) + .setAdapter(ArrayAdapter(host.context, + android.R.layout.simple_list_item_1, stringMentions) + ) { _, which -> + statusActionListener.onViewAccount(mentions[which].id) + } + .show() + .let { forceFocus(it.listView) } + } + + private fun showHashtagsDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val tags = getHashtags(status).map { it.subSequence(1, it.length) }.toList() + AlertDialog.Builder(host.context) + .setTitle(R.string.title_hashtags_dialog) + .setAdapter(ArrayAdapter(host.context, + android.R.layout.simple_list_item_1, tags) + ) { _, which -> + statusActionListener.onViewTag(tags[which].toString()) + } + .show() + .let { forceFocus(it.listView) } + } + + private fun getStatus(childView: View): StatusViewData { + return statusProvider.getStatus(recyclerView.getChildAdapterPosition(childView))!! + } + } + + + private fun getLinks(status: StatusViewData.Concrete): Sequence { + val content = status.content + return if (content is Spannable) { + content.getSpans(0, content.length, URLSpan::class.java) + .asSequence() + .map { span -> + val text = content.subSequence( + content.getSpanStart(span), + content.getSpanEnd(span)) + if (isHashtag(text)) null else LinkSpanInfo(text.toString(), span.url) + } + .filterNotNull() + } else { + emptySequence() + } + } + + private fun getHashtags(status: StatusViewData.Concrete): Sequence { + val content = status.content + return content.getSpans(0, content.length, Object::class.java) + .asSequence() + .map { span -> + content.subSequence(content.getSpanStart(span), content.getSpanEnd(span)) + } + .filter(this::isHashtag) + } + + private fun forceFocus(host: View) { + interrupt() + host.post { + host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) + } + } + + private fun interrupt() { + a11yManager.interrupt() + } + + + private fun isHashtag(text: CharSequence) = text.startsWith("#") + + private val collapseCwAction = AccessibilityActionCompat( + R.id.action_collapse_cw, + context.getString(R.string.status_content_warning_show_less)) + + private val expandCwAction = AccessibilityActionCompat( + R.id.action_expand_cw, + context.getString(R.string.status_content_warning_show_more)) + + private val replyAction = AccessibilityActionCompat( + R.id.action_reply, + context.getString(R.string.action_reply)) + + private val unreblogAction = AccessibilityActionCompat( + R.id.action_unreblog, + context.getString(R.string.action_unreblog)) + + private val reblogAction = AccessibilityActionCompat( + R.id.action_reblog, + context.getString(R.string.action_reblog)) + + private val unfavouriteAction = AccessibilityActionCompat( + R.id.action_unfavourite, + context.getString(R.string.action_unfavourite)) + + private val favouriteAction = AccessibilityActionCompat( + R.id.action_favourite, + context.getString(R.string.action_favourite)) + + private val bookmarkAction = AccessibilityActionCompat( + R.id.action_bookmark, + context.getString(R.string.action_bookmark)) + + private val unbookmarkAction = AccessibilityActionCompat( + R.id.action_unbookmark, + context.getString(R.string.action_bookmark)) + + private val openProfileAction = AccessibilityActionCompat( + R.id.action_open_profile, + context.getString(R.string.action_view_profile)) + + private val linksAction = AccessibilityActionCompat( + R.id.action_links, + context.getString(R.string.action_links)) + + private val mentionsAction = AccessibilityActionCompat( + R.id.action_mentions, + context.getString(R.string.action_mentions)) + + private val hashtagsAction = AccessibilityActionCompat( + R.id.action_hashtags, + context.getString(R.string.action_hashtags)) + + private val openRebloggerAction = AccessibilityActionCompat( + R.id.action_open_reblogger, + context.getString(R.string.action_open_reblogger)) + + private val openRebloggedByAction = AccessibilityActionCompat( + R.id.action_open_reblogged_by, + context.getString(R.string.action_open_reblogged_by)) + + private val openFavsAction = AccessibilityActionCompat( + R.id.action_open_faved_by, + context.getString(R.string.action_open_faved_by)) + + private val moreAction = AccessibilityActionCompat( + R.id.action_more, + context.getString(R.string.action_more) + ) + + private data class LinkSpanInfo(val text: String, val link: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt new file mode 100644 index 0000000..8a5223c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -0,0 +1,55 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +@file:JvmName("ListUtils") + +package com.keylesspalace.tusky.util + +import java.util.LinkedHashSet +import java.util.ArrayList + + +/** + * @return true if list is null or else return list.isEmpty() + */ +fun isEmpty(list: List<*>?): Boolean { + return list == null || list.isEmpty() +} + +/** + * @return a new ArrayList containing the elements without duplicates in the same order + */ +fun removeDuplicates(list: List): ArrayList { + val set = LinkedHashSet(list) + return ArrayList(set) +} + +inline fun List.withoutFirstWhich(predicate: (T) -> Boolean): List { + val newList = toMutableList() + val index = newList.indexOfFirst(predicate) + if (index != -1) { + newList.removeAt(index) + } + return newList +} + +inline fun List.replacedFirstWhich(replacement: T, predicate: (T) -> Boolean): List { + val newList = toMutableList() + val index = newList.indexOfFirst(predicate) + if (index != -1) { + newList[index] = replacement + } + return newList +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt b/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt new file mode 100644 index 0000000..3d4234c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.keylesspalace.tusky.util + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList + +/** + * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system + */ +data class Listing( + // the LiveData of paged lists for the UI to observe + val pagedList: LiveData>, + // represents the network request status to show to the user + val networkState: LiveData, + // represents the refresh status to show to the user. Separate from networkState, this + // value is importantly only when refresh is requested. + val refreshState: LiveData, + // refreshes the whole data and fetches it from scratch. + val refresh: () -> Unit, + // retries any failed requests. + val retry: () -> Unit) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt new file mode 100644 index 0000000..867a5a8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt @@ -0,0 +1,111 @@ +/* Copyright 2019 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.util + +import androidx.lifecycle.* +import io.reactivex.BackpressureStrategy +import io.reactivex.Observable +import io.reactivex.Single + +inline fun LiveData.map(crossinline mapFunction: (X) -> Y): LiveData = + Transformations.map(this) { input -> mapFunction(input) } + +inline fun LiveData.switchMap( + crossinline switchMapFunction: (X) -> LiveData +): LiveData = Transformations.switchMap(this) { input -> switchMapFunction(input) } + +fun LiveData.observeOnce(observer: (T) -> Unit) { + observeForever(object: Observer { + override fun onChanged(value: T) { + removeObserver(this) + observer(value) + } + }) +} + +fun LiveData.observeOnce(owner: LifecycleOwner, observer: (T) -> Unit) { + observe(owner, object: Observer { + override fun onChanged(value: T) { + removeObserver(this) + observer(value) + } + }) +} + +inline fun LiveData.filter(crossinline predicate: (X) -> Boolean): LiveData { + val liveData = MediatorLiveData() + liveData.addSource(this) { value -> + if (predicate(value)) { + liveData.value = value + } + } + return liveData +} + +fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) = + LifecycleContext(this).apply(body) + +class LifecycleContext(val lifecycleOwner: LifecycleOwner) { + inline fun LiveData.observe(crossinline observer: (T) -> Unit) = + this.observe(lifecycleOwner, Observer { observer(it) }) + + /** + * Just hold a subscription, + */ + fun LiveData.subscribe() = + this.observe(lifecycleOwner, Observer { }) +} + +/** + * Invokes @param [combiner] when value of both @param [a] and @param [b] are not null. Returns + * [LiveData] with value set to the result of calling [combiner] with value of both. + * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. + */ +fun combineLiveData(a: LiveData, b: LiveData, combiner: (A, B) -> R): LiveData { + val liveData = MediatorLiveData() + liveData.addSource(a) { + if (a.value != null && b.value != null) { + liveData.value = combiner(a.value!!, b.value!!) + } + } + liveData.addSource(b) { + if (a.value != null && b.value != null) { + liveData.value = combiner(a.value!!, b.value!!) + } + } + return liveData +} + +/** + * Returns [LiveData] with value set to the result of calling [combiner] with value of [a] and [b] + * after either changes. Doesn't check if either has value. + * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. + */ +fun combineOptionalLiveData(a: LiveData, b: LiveData, combiner: (A?, B?) -> R): LiveData { + val liveData = MediatorLiveData() + liveData.addSource(a) { + liveData.value = combiner(a.value, b.value) + } + liveData.addSource(b) { + liveData.value = combiner(a.value, b.value) + } + return liveData +} + +fun Single.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable()) +fun Observable.toLiveData( + backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST +) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST)) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt new file mode 100644 index 0000000..4a80bca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt @@ -0,0 +1,41 @@ +/* Copyright 2019 Mélanie Chauvel (ariasuni) + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import androidx.preference.PreferenceManager +import java.util.* + +class LocaleManager(context: Context) { + + private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + fun setLocale(context: Context): Context { + val language = prefs.getNonNullString("language", "default") + if (language == "default") { + return context + } + val locale = Locale.forLanguageTag(language) + Locale.setDefault(locale) + + val res = context.resources + val config = Configuration(res.configuration) + config.setLocale(locale) + return context.createConfigurationContext(config) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt new file mode 100644 index 0000000..3b1e0fd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt @@ -0,0 +1,230 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.content.ContentResolver +import android.database.Cursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import android.provider.OpenableColumns +import androidx.annotation.Px +import androidx.exifinterface.media.ExifInterface +import android.util.Log +import java.io.* + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +/** + * Helper methods for obtaining and resizing media files + */ +private const val TAG = "MediaUtils" +private const val MEDIA_TEMP_PREFIX = "Share_Media" +const val MEDIA_SIZE_UNKNOWN = -1L + +/** + * Fetches the size of the media represented by the given URI, assuming it is openable and + * the ContentResolver is able to resolve it. + * + * @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN} + */ +fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long { + if(uri == null) { + return MEDIA_SIZE_UNKNOWN + } + + var mediaSize = MEDIA_SIZE_UNKNOWN + val cursor: Cursor? + try { + cursor = contentResolver.query(uri, null, null, null, null) + } catch (e: SecurityException) { + return MEDIA_SIZE_UNKNOWN + } + if (cursor != null) { + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + mediaSize = cursor.getLong(sizeIndex) + cursor.close() + } + return mediaSize +} + +fun getSampledBitmap(contentResolver: ContentResolver, uri: Uri, @Px reqWidth: Int, @Px reqHeight: Int): Bitmap? { + // First decode with inJustDecodeBounds=true to check dimensions + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + var stream: InputStream? + try { + stream = contentResolver.openInputStream(uri) + } catch (e: FileNotFoundException) { + Log.w(TAG, e) + return null + } + + BitmapFactory.decodeStream(stream, null, options) + + IOUtils.closeQuietly(stream) + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false + return try { + stream = contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(stream, null, options) + val orientation = getImageOrientation(uri, contentResolver) + reorientBitmap(bitmap, orientation) + } catch (e: FileNotFoundException) { + Log.w(TAG, e) + null + } catch (e: OutOfMemoryError) { + Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e) + null + } finally { + IOUtils.closeQuietly(stream) + } +} + +@Throws(FileNotFoundException::class) +fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long { + val input = contentResolver.openInputStream(uri) + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeStream(input, null, options) + + IOUtils.closeQuietly(input) + + return (options.outWidth * options.outHeight).toLong() +} + +fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + + val halfHeight = height / 2 + val halfWidth = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize +} + +fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? { + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_NORMAL -> return bitmap + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1.0f, 1.0f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180.0f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> { + matrix.setRotate(180.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.setRotate(90.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90.0f) + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.setRotate(-90.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90.0f) + else -> return bitmap + } + + if (bitmap == null) { + return null + } + + return try { + val result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, + bitmap.height, matrix, true) + if (!bitmap.sameAs(result)) { + bitmap.recycle() + } + result + } catch (e: OutOfMemoryError) { + null + } +} + +fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int { + val inputStream: InputStream? + try { + inputStream = contentResolver.openInputStream(uri) + } catch (e: FileNotFoundException) { + Log.w(TAG, e) + return ExifInterface.ORIENTATION_UNDEFINED + } + if (inputStream == null) { + return ExifInterface.ORIENTATION_UNDEFINED + } + val exifInterface: ExifInterface + try { + exifInterface = ExifInterface(inputStream) + } catch (e: IOException) { + Log.w(TAG, e) + IOUtils.closeQuietly(inputStream) + return ExifInterface.ORIENTATION_UNDEFINED + } + val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + IOUtils.closeQuietly(inputStream) + return orientation +} + +fun deleteStaleCachedMedia(mediaDirectory: File?) { + if (mediaDirectory == null || !mediaDirectory.exists()) { + // Nothing to do + return + } + + val twentyfourHoursAgo = Calendar.getInstance() + twentyfourHoursAgo.add(Calendar.HOUR, -24) + val unixTime = twentyfourHoursAgo.timeInMillis + + val files = mediaDirectory.listFiles{ file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) } + if (files == null || files.isEmpty()) { + // Nothing to do + return + } + + for (file in files) { + try { + file.delete() + } catch (se: SecurityException) { + Log.e(TAG, "Error removing stale cached media") + } + } +} + +fun getTemporaryMediaFilename(extension: String): String { + return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.$extension" +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt b/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt new file mode 100644 index 0000000..09a0033 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.keylesspalace.tusky.util + +enum class Status { + RUNNING, + SUCCESS, + FAILED +} + +@Suppress("DataClassPrivateConstructor") +data class NetworkState private constructor( + val status: Status, + val msg: String? = null) { + companion object { + val LOADED = NetworkState(Status.SUCCESS) + val LOADING = NetworkState(Status.RUNNING) + fun error(msg: String?) = NetworkState(Status.FAILED, msg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt new file mode 100644 index 0000000..65c8f6c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt @@ -0,0 +1,45 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import com.keylesspalace.tusky.entity.Notification +import org.json.JSONArray + +/** + * Serialize to string array and deserialize notifications type + */ + +fun serialize(data: Set?): String { + val array = JSONArray() + data?.forEach { + array.put(it.presentation) + } + return array.toString() +} + +fun deserialize(data: String?): Set { + val ret = HashSet() + data?.let { + val array = JSONArray(data) + for (i in 0 until array.length()) { + val item = array.getString(i) + val type = Notification.Type.byString(item) + if (type != Notification.Type.UNKNOWN) + ret.add(type) + } + } + return ret +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java new file mode 100644 index 0000000..b4adae5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java @@ -0,0 +1,89 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Tusky. If + * not, see . */ + +package com.keylesspalace.tusky.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import com.keylesspalace.tusky.BuildConfig; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.concurrent.TimeUnit; + +import okhttp3.Cache; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.brotli.BrotliInterceptor; + +public class OkHttpUtils { + + @NonNull + public static OkHttpClient.Builder getCompatibleClientBuilder(@NonNull Context context) { + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + boolean httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false); + String httpServer = preferences.getString("httpProxyServer", ""); + int httpPort; + try { + httpPort = Integer.parseInt(preferences.getString("httpProxyPort", "-1")); + } catch (NumberFormatException e) { + // user has entered wrong port, fall back to no proxy + httpPort = -1; + } + + int cacheSize = 25*1024*1024; // 25 MiB + + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .addInterceptor(getUserAgentInterceptor()) + .addInterceptor(BrotliInterceptor.INSTANCE) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cache(new Cache(context.getCacheDir(), cacheSize)); + + if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0) && (httpPort < 65535)) { + InetSocketAddress address = InetSocketAddress.createUnresolved(httpServer, httpPort); + builder.proxy(new Proxy(Proxy.Type.HTTP, address)); + } + + return builder; + } + + /** + * Add a custom User-Agent that contains Tusky & Android Version to all requests + * Example: + * User-Agent: Tusky/1.1.2 Android/5.0.2 + */ + @NonNull + private static Interceptor getUserAgentInterceptor() { + return chain -> { + Request originalRequest = chain.request(); + Request requestWithUserAgent = originalRequest.newBuilder() + .header("User-Agent", BuildConfig.APPLICATION_NAME + "/"+ BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE) + .build(); + return chain.proceed(requestWithUserAgent); + }; + } + +} + + diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OmittedDomainFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/util/OmittedDomainFetcher.kt new file mode 100644 index 0000000..60a4e1e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/OmittedDomainFetcher.kt @@ -0,0 +1,58 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.HttpUrlFetcher +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.load.model.stream.HttpGlideUrlLoader +import com.bumptech.glide.module.AppGlideModule +import com.bumptech.glide.signature.ObjectKey +import com.keylesspalace.tusky.TuskyApplication +import com.keylesspalace.tusky.db.AccountManager +import java.io.File +import java.io.InputStream +import javax.inject.Inject + +@GlideModule +class OmittedDomainAppModule : AppGlideModule() { + @Inject + lateinit var accountManager : AccountManager + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + (context.applicationContext as TuskyApplication).androidInjector.inject(this) + + registry.append(String::class.java, InputStream::class.java, OmittedDomainLoaderFactory(accountManager)) + } +} + +class OmittedDomainLoaderFactory(val accountManager: AccountManager) : ModelLoaderFactory { + override fun teardown() = Unit + + override fun build(factory: MultiModelLoaderFactory): ModelLoader = OmittedDomainLoader(accountManager) +} + +class OmittedDomainLoader(val accountManager: AccountManager) : ModelLoader { + override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData? + { + val trueUrl = if(accountManager.activeAccount != null) + "https://" + accountManager.activeAccount!!.domain + model + else model + + val timeout = options.get(HttpGlideUrlLoader.TIMEOUT) ?: 100 + + return ModelLoader.LoadData(ObjectKey(model), HttpUrlFetcher(GlideUrl(trueUrl), timeout)) + } + + + override fun handles(model: String): Boolean { + val file = File(model) + return !file.exists() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java new file mode 100644 index 0000000..4f7d3ef --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java @@ -0,0 +1,491 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.keylesspalace.tusky.util; + +import androidx.annotation.AnyThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +/** + * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and + * {@link androidx.paging.DataSource}s to help with tracking network requests. + *

+ * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, + * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request + * for each of them via {@link #runIfNotRunning(RequestType, Request)}. + *

+ * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. + *

+ * A sample usage of this class to limit requests looks like this: + *

+ * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
+ *     // TODO replace with an executor from your application
+ *     Executor executor = Executors.newSingleThreadExecutor();
+ *     PagingRequestHelper helper = new PagingRequestHelper(executor);
+ *     // imaginary API service, using Retrofit
+ *     MyApi api;
+ *
+ *     {@literal @}Override
+ *     public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
+ *         helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
+ *                 helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
+ *                         new Callback<ApiResponse>() {
+ *                             {@literal @}Override
+ *                             public void onResponse(Call<ApiResponse> call,
+ *                                     Response<ApiResponse> response) {
+ *                                 // TODO insert new records into database
+ *                                 helperCallback.recordSuccess();
+ *                             }
+ *
+ *                             {@literal @}Override
+ *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
+ *                                 helperCallback.recordFailure(t);
+ *                             }
+ *                         }));
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
+ *         helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
+ *                 helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
+ *                         new Callback<ApiResponse>() {
+ *                             {@literal @}Override
+ *                             public void onResponse(Call<ApiResponse> call,
+ *                                     Response<ApiResponse> response) {
+ *                                 // TODO insert new records into database
+ *                                 helperCallback.recordSuccess();
+ *                             }
+ *
+ *                             {@literal @}Override
+ *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
+ *                                 helperCallback.recordFailure(t);
+ *                             }
+ *                         }));
+ *     }
+ * }
+ * 
+ *

+ * The helper provides an API to observe combined request status, which can be reported back to the + * application based on your business rules. + *

+ * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
+ * helper.addListener(status -> {
+ *     // merge multiple states per request type into one, or dispatch separately depending on
+ *     // your application logic.
+ *     if (status.hasRunning()) {
+ *         combined.postValue(PagingRequestHelper.Status.RUNNING);
+ *     } else if (status.hasError()) {
+ *         // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
+ *         combined.postValue(PagingRequestHelper.Status.FAILED);
+ *     } else {
+ *         combined.postValue(PagingRequestHelper.Status.SUCCESS);
+ *     }
+ * });
+ * 
+ */ +// THIS class is likely to be moved into the library in a future release. Feel free to copy it +// from this sample. +public class PagingRequestHelper { + private final Object mLock = new Object(); + private final Executor mRetryService; + @GuardedBy("mLock") + private final RequestQueue[] mRequestQueues = new RequestQueue[] + {new RequestQueue(RequestType.INITIAL), + new RequestQueue(RequestType.BEFORE), + new RequestQueue(RequestType.AFTER)}; + @NonNull + final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); + /** + * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run + * retry actions. + * + * @param retryService The {@link Executor} that can run the retry actions. + */ + public PagingRequestHelper(@NonNull Executor retryService) { + mRetryService = retryService; + } + /** + * Adds a new listener that will be notified when any request changes {@link Status state}. + * + * @param listener The listener that will be notified each time a request's status changes. + * @return True if it is added, false otherwise (e.g. it already exists in the list). + */ + @AnyThread + public boolean addListener(@NonNull Listener listener) { + return mListeners.add(listener); + } + /** + * Removes the given listener from the listeners list. + * + * @param listener The listener that will be removed. + * @return True if the listener is removed, false otherwise (e.g. it never existed) + */ + public boolean removeListener(@NonNull Listener listener) { + return mListeners.remove(listener); + } + /** + * Runs the given {@link Request} if no other requests in the given request type is already + * running. + *

+ * If run, the request will be run in the current thread. + * + * @param type The type of the request. + * @param request The request to run. + * @return True if the request is run, false otherwise. + */ + @SuppressWarnings("WeakerAccess") + @AnyThread + public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) { + boolean hasListeners = !mListeners.isEmpty(); + StatusReport report = null; + synchronized (mLock) { + RequestQueue queue = mRequestQueues[type.ordinal()]; + if (queue.mRunning != null) { + return false; + } + queue.mRunning = request; + queue.mStatus = Status.RUNNING; + queue.mFailed = null; + queue.mLastError = null; + if (hasListeners) { + report = prepareStatusReportLocked(); + } + } + if (report != null) { + dispatchReport(report); + } + final RequestWrapper wrapper = new RequestWrapper(request, this, type); + wrapper.run(); + return true; + } + @GuardedBy("mLock") + private StatusReport prepareStatusReportLocked() { + Throwable[] errors = new Throwable[]{ + mRequestQueues[0].mLastError, + mRequestQueues[1].mLastError, + mRequestQueues[2].mLastError + }; + return new StatusReport( + getStatusForLocked(RequestType.INITIAL), + getStatusForLocked(RequestType.BEFORE), + getStatusForLocked(RequestType.AFTER), + errors + ); + } + @GuardedBy("mLock") + private Status getStatusForLocked(RequestType type) { + return mRequestQueues[type.ordinal()].mStatus; + } + @AnyThread + @VisibleForTesting + void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) { + StatusReport report = null; + final boolean success = throwable == null; + boolean hasListeners = !mListeners.isEmpty(); + synchronized (mLock) { + RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; + queue.mRunning = null; + queue.mLastError = throwable; + if (success) { + queue.mFailed = null; + queue.mStatus = Status.SUCCESS; + } else { + queue.mFailed = wrapper; + queue.mStatus = Status.FAILED; + } + if (hasListeners) { + report = prepareStatusReportLocked(); + } + } + if (report != null) { + dispatchReport(report); + } + } + private void dispatchReport(StatusReport report) { + for (Listener listener : mListeners) { + listener.onStatusChange(report); + } + } + /** + * Retries all failed requests. + * + * @return True if any request is retried, false otherwise. + */ + public boolean retryAllFailed() { + final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; + boolean retried = false; + synchronized (mLock) { + for (int i = 0; i < RequestType.values().length; i++) { + toBeRetried[i] = mRequestQueues[i].mFailed; + mRequestQueues[i].mFailed = null; + } + } + for (RequestWrapper failed : toBeRetried) { + if (failed != null) { + failed.retry(mRetryService); + retried = true; + } + } + return retried; + } + static class RequestWrapper implements Runnable { + @NonNull + final Request mRequest; + @NonNull + final PagingRequestHelper mHelper; + @NonNull + final RequestType mType; + RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper, + @NonNull RequestType type) { + mRequest = request; + mHelper = helper; + mType = type; + } + @Override + public void run() { + mRequest.run(new Request.Callback(this, mHelper)); + } + void retry(Executor service) { + service.execute(new Runnable() { + @Override + public void run() { + mHelper.runIfNotRunning(mType, mRequest); + } + }); + } + } + /** + * Runner class that runs a request tracked by the {@link PagingRequestHelper}. + *

+ * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} + * or {@link Callback#recordSuccess()} once and only once. This call + * can be made any time. Until that method call is made, {@link PagingRequestHelper} will + * consider the request is running. + */ + @FunctionalInterface + public interface Request { + /** + * Should run the request and call the given {@link Callback} with the result of the + * request. + * + * @param callback The callback that should be invoked with the result. + */ + void run(Callback callback); + /** + * Callback class provided to the {@link #run(Callback)} method to report the result. + */ + class Callback { + private final AtomicBoolean mCalled = new AtomicBoolean(); + private final RequestWrapper mWrapper; + private final PagingRequestHelper mHelper; + Callback(RequestWrapper wrapper, PagingRequestHelper helper) { + mWrapper = wrapper; + mHelper = helper; + } + /** + * Call this method when the request succeeds and new data is fetched. + */ + @SuppressWarnings("unused") + public final void recordSuccess() { + if (mCalled.compareAndSet(false, true)) { + mHelper.recordResult(mWrapper, null); + } else { + throw new IllegalStateException( + "already called recordSuccess or recordFailure"); + } + } + /** + * Call this method with the failure message and the request can be retried via + * {@link #retryAllFailed()}. + * + * @param throwable The error that occured while carrying out the request. + */ + @SuppressWarnings("unused") + public final void recordFailure(@NonNull Throwable throwable) { + //noinspection ConstantConditions + if (throwable == null) { + throw new IllegalArgumentException("You must provide a throwable describing" + + " the error to record the failure"); + } + if (mCalled.compareAndSet(false, true)) { + mHelper.recordResult(mWrapper, throwable); + } else { + throw new IllegalStateException( + "already called recordSuccess or recordFailure"); + } + } + } + } + /** + * Data class that holds the information about the current status of the ongoing requests + * using this helper. + */ + public static final class StatusReport { + /** + * Status of the latest request that were submitted with {@link RequestType#INITIAL}. + */ + @NonNull + public final Status initial; + /** + * Status of the latest request that were submitted with {@link RequestType#BEFORE}. + */ + @NonNull + public final Status before; + /** + * Status of the latest request that were submitted with {@link RequestType#AFTER}. + */ + @NonNull + public final Status after; + @NonNull + private final Throwable[] mErrors; + StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, + @NonNull Throwable[] errors) { + this.initial = initial; + this.before = before; + this.after = after; + this.mErrors = errors; + } + /** + * Convenience method to check if there are any running requests. + * + * @return True if there are any running requests, false otherwise. + */ + public boolean hasRunning() { + return initial == Status.RUNNING + || before == Status.RUNNING + || after == Status.RUNNING; + } + /** + * Convenience method to check if there are any requests that resulted in an error. + * + * @return True if there are any requests that finished with error, false otherwise. + */ + public boolean hasError() { + return initial == Status.FAILED + || before == Status.FAILED + || after == Status.FAILED; + } + /** + * Returns the error for the given request type. + * + * @param type The request type for which the error should be returned. + * @return The {@link Throwable} returned by the failing request with the given type or + * {@code null} if the request for the given type did not fail. + */ + @Nullable + public Throwable getErrorFor(@NonNull RequestType type) { + return mErrors[type.ordinal()]; + } + @Override + public String toString() { + return "StatusReport{" + + "initial=" + initial + + ", before=" + before + + ", after=" + after + + ", mErrors=" + Arrays.toString(mErrors) + + '}'; + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StatusReport that = (StatusReport) o; + if (initial != that.initial) return false; + if (before != that.before) return false; + if (after != that.after) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(mErrors, that.mErrors); + } + @Override + public int hashCode() { + int result = initial.hashCode(); + result = 31 * result + before.hashCode(); + result = 31 * result + after.hashCode(); + result = 31 * result + Arrays.hashCode(mErrors); + return result; + } + } + /** + * Listener interface to get notified by request status changes. + */ + public interface Listener { + /** + * Called when the status for any of the requests has changed. + * + * @param report The current status report that has all the information about the requests. + */ + void onStatusChange(@NonNull StatusReport report); + } + /** + * Represents the status of a Request for each {@link RequestType}. + */ + public enum Status { + /** + * There is current a running request. + */ + RUNNING, + /** + * The last request has succeeded or no such requests have ever been run. + */ + SUCCESS, + /** + * The last request has failed. + */ + FAILED + } + /** + * Available request types. + */ + public enum RequestType { + /** + * Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for + * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + */ + INITIAL, + /** + * Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or + * {@code onItemAtFrontLoaded} in + * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + */ + BEFORE, + /** + * Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or + * {@code onItemAtEndLoaded} in + * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + */ + AFTER + } + class RequestQueue { + @NonNull + final RequestType mRequestType; + @Nullable + RequestWrapper mFailed; + @Nullable + Request mRunning; + @Nullable + Throwable mLastError; + @NonNull + Status mStatus = Status.SUCCESS; + RequestQueue(@NonNull RequestType requestType) { + mRequestType = requestType; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java b/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java new file mode 100644 index 0000000..a0880a5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java @@ -0,0 +1,94 @@ +package com.keylesspalace.tusky.util; + +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + + +/** + * This list implementation can help to keep two lists in sync - like real models and view models. + * Every operation on the main list triggers update of the supplementary list (but not vice versa). + * This makes sure that the main list is always the source of truth. + * Main list is projected to the supplementary list by the passed mapper function. + * Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()}, + * {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the + * supplementary list size so lists are always have the same length. + * This implementation will not try to recover from exceptional cases so lists may be out of sync + * after the exception. + * + * It is most useful with immutable data because we cannot track changes inside stored objects. + * @param type of elements in the main list + * @param type of elements in supplementary list + */ +public final class PairedList extends AbstractList { + private final List main = new ArrayList<>(); + private final List synced = new ArrayList<>(); + private final Function mapper; + + /** + * Construct new paired list. Main and supplementary lists will be empty. + * @param mapper Function, which will be used to translate items from the main list to the + * supplementary one. + */ + public PairedList(Function mapper) { + this.mapper = mapper; + } + + public List getPairedCopy() { + return new ArrayList<>(synced); + } + + public V getPairedItem(int index) { + return synced.get(index); + } + + @Nullable + public V getPairedItemOrNull(int index) { + if (index >= 0 && index < synced.size()) { + return synced.get(index); + } else { + return null; + } + } + + public void setPairedItem(int index, V element) { + synced.set(index, element); + } + + @Override + public T get(int index) { + return main.get(index); + } + + @Override + public T set(int index, T element) { + synced.set(index, mapper.apply(element)); + return main.set(index, element); + } + + @Override + public boolean add(T t) { + synced.add(mapper.apply(t)); + return main.add(t); + } + + @Override + public void add(int index, T element) { + synced.add(index, mapper.apply(element)); + main.add(index, element); + } + + @Override + public T remove(int index) { + synced.remove(index); + return main.remove(index); + } + + @Override + public int size() { + return main.size(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt new file mode 100644 index 0000000..1f9f35d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt @@ -0,0 +1,13 @@ +package com.keylesspalace.tusky.util + +sealed class Resource(open val data: T?) + +class Loading (override val data: T? = null) : Resource(data) + +class Success (override val data: T? = null) : Resource(data) + +class Error (override val data: T? = null, + val errorMessage: String? = null, + var consumed: Boolean = false, + val cause: Throwable? = null +): Resource(data) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt new file mode 100644 index 0000000..c78b0f7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt @@ -0,0 +1,18 @@ +package com.keylesspalace.tusky.util + +import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +open class RxAwareViewModel : ViewModel() { + val disposables = CompositeDisposable() + + fun Disposable.autoDispose() = disposables.add(this) + + @CallSuper + override fun onCleared() { + super.onCleared() + disposables.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java new file mode 100644 index 0000000..7911a61 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java @@ -0,0 +1,57 @@ +package com.keylesspalace.tusky.util; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.keylesspalace.tusky.db.AppDatabase; +import com.keylesspalace.tusky.db.TootDao; +import com.keylesspalace.tusky.db.TootEntity; + +import java.util.ArrayList; + +import javax.inject.Inject; + +public final class SaveTootHelper { + + private static final String TAG = "SaveTootHelper"; + + private TootDao tootDao; + private Context context; + private Gson gson = new Gson(); + + @Inject + public SaveTootHelper(@NonNull AppDatabase appDatabase, @NonNull Context context) { + this.tootDao = appDatabase.tootDao(); + this.context = context; + } + + public void deleteDraft(int tootId) { + TootEntity item = tootDao.find(tootId); + if (item != null) { + deleteDraft(item); + } + } + + public void deleteDraft(@NonNull TootEntity item) { + // Delete any media files associated with the status. + ArrayList uris = gson.fromJson(item.getUrls(), + new TypeToken>() { + }.getType()); + if (uris != null) { + for (String uriString : uris) { + Uri uri = Uri.parse(uriString); + if (context.getContentResolver().delete(uri, null, null) == 0) { + Log.e(TAG, String.format("Did not delete file %s.", uriString)); + } + } + } + // update DB + tootDao.delete(item.getUid()); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt new file mode 100644 index 0000000..1acf226 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt @@ -0,0 +1,103 @@ +/* Copyright 2019 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 . */ + +@file:JvmName("ShareShortcutHelper") + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.text.TextUtils +import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountEntity +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers + +fun updateShortcut(context: Context, account: AccountEntity) { + + Single.fromCallable { + + val innerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_inner_size) + val outerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_outer_size) + + val bmp = if (TextUtils.isEmpty(account.profilePictureUrl)) { + Glide.with(context) + .asBitmap() + .load(R.drawable.avatar_default) + .submit(innerSize, innerSize) + .get() + } else { + Glide.with(context) + .asBitmap() + .load(account.profilePictureUrl) + .error(R.drawable.avatar_default) + .submit(innerSize, innerSize) + .get() + } + + // inset the loaded bitmap inside a 108dp transparent canvas so it looks good as adaptive icon + val outBmp = Bitmap.createBitmap(outerSize, outerSize, Bitmap.Config.ARGB_8888) + + val canvas = Canvas(outBmp) + canvas.drawBitmap(bmp, (outerSize - innerSize).toFloat() / 2f, (outerSize - innerSize).toFloat() / 2f, null) + + val icon = IconCompat.createWithAdaptiveBitmap(outBmp) + + val person = Person.Builder() + .setIcon(icon) + .setName(account.displayName) + .setKey(account.identifier) + .build() + + // This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different + val intent = Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(NotificationHelper.ACCOUNT_ID, account.id) + } + + val shortcutInfo = ShortcutInfoCompat.Builder(context, account.id.toString()) + .setIntent(intent) + .setCategories(setOf("com.keylesspalace.tusky.Share")) + .setShortLabel(account.displayName) + .setPerson(person) + .setLongLived(true) + .setIcon(icon) + .build() + + ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo)) + + } + .subscribeOn(Schedulers.io()) + .onErrorReturnItem(false) + .subscribe() + + +} + +fun removeShortcut(context: Context, account: AccountEntity) { + + ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(account.id.toString())) + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt new file mode 100644 index 0000000..a3835a8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.util + +import android.content.SharedPreferences + +fun SharedPreferences.getNonNullString(key: String, defValue: String): String { + return this.getString(key, defValue) ?: defValue +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt new file mode 100644 index 0000000..ba9c420 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt @@ -0,0 +1,111 @@ +/* Copyright 2019 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.util + +import android.text.InputFilter +import android.text.SpannableStringBuilder +import android.text.Spanned + +/** + * Defines how many characters to extend beyond the limit to cut at the end of the word on the + * boundary of it rather than cutting at the word preceding that one. + */ +private const val RUNWAY = 10 + +/** + * Default for maximum status length on Mastodon and default collapsing length on Pleroma. + */ +private const val LENGTH_DEFAULT = 500 + +/** + * Calculates if it's worth trimming the message at a specific limit or if the content that will + * be hidden will not be enough to justify the operation. + * + * @param message The message to trim. + * @return Whether the message should be trimmed or not. + */ +fun shouldTrimStatus(message: Spanned): Boolean { + return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75 +} + +/** + * A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter + * constraints and adds better visuals such as: + *

    + *
  • Ellipsis at the end of the constrained text to show continuation.
  • + *
  • Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.
  • + *
  • Constraints end at the end of the last "word", before a whitespace.
  • + *
  • Expansion of the limit by up to 10 characters to facilitate the previous constraint.
  • + *
  • Constraints are not applied if the percentage of hidden content is too small.
  • + *
+ */ +object SmartLengthInputFilter : InputFilter { + /** {@inheritDoc} */ + override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { + // Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin. + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 + + val sourceLength = source.length + var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart)) + if (keep <= 0) return "" + if (keep >= end - start) return null // Keep original + + keep += start + + // Skip trimming if the ratio doesn't warrant it + if (keep.toDouble() / sourceLength > 0.75) return null + + // Enable trimming at the end of the closest word if possible + if (source[keep].isLetterOrDigit()) { + var boundary: Int + + // Android N+ offer a clone of the ICU APIs in Java for better internationalization and + // unicode support. Using the ICU version of BreakIterator grants better support for + // those without having to add the ICU4J library at a minimum Api trade-off. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + val iterator = android.icu.text.BreakIterator.getWordInstance() + iterator.setText(source.toString()) + boundary = iterator.following(keep) + if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) + } else { + val iterator = java.text.BreakIterator.getWordInstance() + iterator.setText(source.toString()) + boundary = iterator.following(keep) + if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) + } + + keep = boundary + } else { + + // If no runway is allowed simply remove whitespaces if present + while(source[keep - 1].isWhitespace()) { + --keep + if (keep == start) return "" + } + } + + if (source[keep - 1].isHighSurrogate()) { + --keep + if (keep == start) return "" + } + + return if (source is Spanned) { + SpannableStringBuilder(source, start, keep).append("…") + } else { + "${source.subSequence(start, keep)}…" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt new file mode 100644 index 0000000..ca68152 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -0,0 +1,161 @@ +package com.keylesspalace.tusky.util + +import android.text.Spannable +import android.text.Spanned +import android.text.style.CharacterStyle +import android.text.style.ForegroundColorSpan +import android.text.style.URLSpan +import java.util.regex.Pattern +import kotlin.math.max + +/** + * @see
+ * Tag#HASHTAG_RE. + */ +private const val TAG_REGEX = "(?:^|[^/)A-Za-z0-9_])#([\\w_]*[\\p{Alpha}_][\\w_]*)" + +/** + * @see + * Account#MENTION_RE + */ +private const val MENTION_REGEX = "(?:^|[^/[:word:]])@([a-z\\d_-]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)" + +private const val HTTP_URL_REGEX = "(?:(^|\\b)http://[^\\s]+)" +private const val HTTPS_URL_REGEX = "(?:(^|\\b)https://[^\\s]+)" + +/** + * Dump of android.util.Patterns.WEB_URL + */ +private val STRICT_WEB_URL_PATTERN = Pattern.compile("(((?:(?i:http|https|rtsp)://(?:(?:[a-zA-Z0-9\\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?(?:(([a-zA-Z0-9[ -\uD7FF豈-\uFDCFﷰ-\uFFEF\uD800\uDC00-\uD83F\uDFFD\uD840\uDC00-\uD87F\uDFFD\uD880\uDC00-\uD8BF\uDFFD\uD8C0\uDC00-\uD8FF\uDFFD\uD900\uDC00-\uD93F\uDFFD\uD940\uDC00-\uD97F\uDFFD\uD980\uDC00-\uD9BF\uDFFD\uD9C0\uDC00-\uD9FF\uDFFD\uDA00\uDC00-\uDA3F\uDFFD\uDA40\uDC00-\uDA7F\uDFFD\uDA80\uDC00-\uDABF\uDFFD\uDAC0\uDC00-\uDAFF\uDFFD\uDB00\uDC00-\uDB3F\uDFFD\uDB44\uDC00-\uDB7F\uDFFD&&[^ [ - ]\u2028\u2029  ]]](?:[a-zA-Z0-9[ -\uD7FF豈-\uFDCFﷰ-\uFFEF\uD800\uDC00-\uD83F\uDFFD\uD840\uDC00-\uD87F\uDFFD\uD880\uDC00-\uD8BF\uDFFD\uD8C0\uDC00-\uD8FF\uDFFD\uD900\uDC00-\uD93F\uDFFD\uD940\uDC00-\uD97F\uDFFD\uD980\uDC00-\uD9BF\uDFFD\uD9C0\uDC00-\uD9FF\uDFFD\uDA00\uDC00-\uDA3F\uDFFD\uDA40\uDC00-\uDA7F\uDFFD\uDA80\uDC00-\uDABF\uDFFD\uDAC0\uDC00-\uDAFF\uDFFD\uDB00\uDC00-\uDB3F\uDFFD\uDB44\uDC00-\uDB7F\uDFFD&&[^ [ - ]\u2028\u2029  ]]_\\-]{0,61}[a-zA-Z0-9[ -\uD7FF豈-\uFDCFﷰ-\uFFEF\uD800\uDC00-\uD83F\uDFFD\uD840\uDC00-\uD87F\uDFFD\uD880\uDC00-\uD8BF\uDFFD\uD8C0\uDC00-\uD8FF\uDFFD\uD900\uDC00-\uD93F\uDFFD\uD940\uDC00-\uD97F\uDFFD\uD980\uDC00-\uD9BF\uDFFD\uD9C0\uDC00-\uD9FF\uDFFD\uDA00\uDC00-\uDA3F\uDFFD\uDA40\uDC00-\uDA7F\uDFFD\uDA80\uDC00-\uDABF\uDFFD\uDAC0\uDC00-\uDAFF\uDFFD\uDB00\uDC00-\uDB3F\uDFFD\uDB44\uDC00-\uDB7F\uDFFD&&[^ [ - ]\u2028\u2029  ]]]){0,1}\\.)+(xn\\-\\-[\\w\\-]{0,58}\\w|[a-zA-Z[ -\uD7FF豈-\uFDCFﷰ-\uFFEF\uD800\uDC00-\uD83F\uDFFD\uD840\uDC00-\uD87F\uDFFD\uD880\uDC00-\uD8BF\uDFFD\uD8C0\uDC00-\uD8FF\uDFFD\uD900\uDC00-\uD93F\uDFFD\uD940\uDC00-\uD97F\uDFFD\uD980\uDC00-\uD9BF\uDFFD\uD9C0\uDC00-\uD9FF\uDFFD\uDA00\uDC00-\uDA3F\uDFFD\uDA40\uDC00-\uDA7F\uDFFD\uDA80\uDC00-\uDABF\uDFFD\uDAC0\uDC00-\uDAFF\uDFFD\uDB00\uDC00-\uDB3F\uDFFD\uDB44\uDC00-\uDB7F\uDFFD&&[^ [ - ]\u2028\u2029  ]]]{2,63})|((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[0-9]))))(?:\\:\\d{1,5})?)([/\\?](?:(?:[a-zA-Z0-9[ -\uD7FF豈-\uFDCFﷰ-\uFFEF\uD800\uDC00-\uD83F\uDFFD\uD840\uDC00-\uD87F\uDFFD\uD880\uDC00-\uD8BF\uDFFD\uD8C0\uDC00-\uD8FF\uDFFD\uD900\uDC00-\uD93F\uDFFD\uD940\uDC00-\uD97F\uDFFD\uD980\uDC00-\uD9BF\uDFFD\uD9C0\uDC00-\uD9FF\uDFFD\uDA00\uDC00-\uDA3F\uDFFD\uDA40\uDC00-\uDA7F\uDFFD\uDA80\uDC00-\uDABF\uDFFD\uDAC0\uDC00-\uDAFF\uDFFD\uDB00\uDC00-\uDB3F\uDFFD\uDB44\uDC00-\uDB7F\uDFFD&&[^ [ - ]\u2028\u2029  ]];/\\?:@&=#~\\-\\.\\+!\\*'\\(\\),_\\\$])|(?:%[a-fA-F0-9]{2}))*)?(?:\\b|\$|^))") + +private val spanClasses = listOf(ForegroundColorSpan::class.java, URLSpan::class.java) +private val finders = mapOf( + FoundMatchType.HTTP_URL to PatternFinder(':', HTTP_URL_REGEX, 5, Character::isWhitespace), + FoundMatchType.HTTPS_URL to PatternFinder(':', HTTPS_URL_REGEX, 6, Character::isWhitespace), + FoundMatchType.TAG to PatternFinder('#', TAG_REGEX, 1, ::isValidForTagPrefix), + FoundMatchType.MENTION to PatternFinder('@', MENTION_REGEX, 1, Character::isWhitespace) // TODO: We also need a proper validator for mentions +) + +private enum class FoundMatchType { + HTTP_URL, + HTTPS_URL, + TAG, + MENTION, +} + +private class FindCharsResult { + lateinit var matchType: FoundMatchType + var start: Int = -1 + var end: Int = -1 +} + +private class PatternFinder(val searchCharacter: Char, regex: String, val searchPrefixWidth: Int, + val prefixValidator: (Int) -> Boolean) { + val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) +} + +private fun clearSpans(text: Spannable, spanClass: Class) { + for(span in text.getSpans(0, text.length, spanClass)) { + text.removeSpan(span) + } +} + +private fun findPattern(string: String, fromIndex: Int): FindCharsResult { + val result = FindCharsResult() + for (i in fromIndex..string.lastIndex) { + val c = string[i] + for (matchType in FoundMatchType.values()) { + val finder = finders[matchType] + if (finder!!.searchCharacter == c + && ((i - fromIndex) < finder.searchPrefixWidth || + finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth)))) { + result.matchType = matchType + result.start = max(0, i - finder.searchPrefixWidth) + findEndOfPattern(string, result, finder.pattern) + if (result.start + finder.searchPrefixWidth <= i + 1 && // The found result is actually triggered by the correct search character + result.end >= result.start) { // ...and we actually found a valid result + return result + } + } + } + } + return result +} + +private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: Pattern) { + val matcher = pattern.matcher(string) + if (matcher.find(result.start)) { + // Once we have API level 26+, we can use named captures... + val end = matcher.end() + result.start = matcher.start() + when (result.matchType) { + FoundMatchType.TAG -> { + if (isValidForTagPrefix(string.codePointAt(result.start))) { + if (string[result.start] != '#' || + (string[result.start] == '#' && string[result.start + 1] == '#')) { + ++result.start + } + } + } + else -> { + if (Character.isWhitespace(string.codePointAt(result.start))) { + ++result.start + } + } + } + when (result.matchType) { + FoundMatchType.HTTP_URL, FoundMatchType.HTTPS_URL -> { + // Preliminary url patterns are fast/permissive, now we'll do full validation + if (STRICT_WEB_URL_PATTERN.matcher(string.substring(result.start, end)).matches()) { + result.end = end + } + } + else -> result.end = end + } + } +} + +private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, start: Int, end: Int): CharacterStyle { + return when(matchType) { + FoundMatchType.HTTP_URL -> CustomURLSpan(string.substring(start, end)) + FoundMatchType.HTTPS_URL -> CustomURLSpan(string.substring(start, end)) + else -> ForegroundColorSpan(colour) + } +} + +/** Takes text containing mentions and hashtags and urls and makes them the given colour. */ +fun highlightSpans(text: Spannable, colour: Int) { + // Strip all existing colour spans. + for (spanClass in spanClasses) { + clearSpans(text, spanClass) + } + + // Colour the mentions and hashtags. + val string = text.toString() + val length = text.length + var start = 0 + var end = 0 + while (end >= 0 && end < length && start >= 0) { + // Search for url first because it can contain the other characters + val found = findPattern(string, end) + start = found.start + end = found.end + if (start >= 0 && end > start) { + text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + start += finders[found.matchType]!!.searchPrefixWidth + } + } +} + +private fun isWordCharacters(codePoint: Int): Boolean { + return (codePoint in 0x30..0x39) || // [0-9] + (codePoint in 0x41..0x5a) || // [A-Z] + (codePoint == 0x5f) || // _ + (codePoint in 0x61..0x7a) // [a-z] +} + +private fun isValidForTagPrefix(codePoint: Int): Boolean { + return !(isWordCharacters(codePoint) || // \w + (codePoint == 0x2f) || // / + (codePoint == 0x29)) // ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt new file mode 100644 index 0000000..7321605 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt @@ -0,0 +1,22 @@ +package com.keylesspalace.tusky.util + +data class StatusDisplayOptions( + @get:JvmName("animateAvatars") + val animateAvatars: Boolean, + @get:JvmName("mediaPreviewEnabled") + val mediaPreviewEnabled: Boolean, + @get:JvmName("useAbsoluteTime") + val useAbsoluteTime: Boolean, + @get:JvmName("showBotOverlay") + val showBotOverlay: Boolean, + @get:JvmName("useBlurhash") + val useBlurhash: Boolean, + @get:JvmName("cardViewMode") + val cardViewMode: CardViewMode, + @get:JvmName("confirmReblogs") + val confirmReblogs: Boolean, + @get:JvmName("renderStatusAsMention") + val renderStatusAsMention: Boolean, + @get:JvmName("hideStats") + val hideStats: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt new file mode 100644 index 0000000..7b0cc34 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -0,0 +1,331 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.text.InputFilter +import android.text.TextUtils +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.view.MediaPreviewImageView +import com.keylesspalace.tusky.viewdata.PollViewData +import com.keylesspalace.tusky.viewdata.buildDescription +import com.keylesspalace.tusky.viewdata.calculatePercent +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.min + +class StatusViewHelper(private val itemView: View) { + interface MediaPreviewListener { + fun onViewMedia(v: View?, idx: Int) + fun onContentHiddenChange(isShowing: Boolean) + } + + private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) + + fun setMediasPreview( + statusDisplayOptions: StatusDisplayOptions, + attachments: List, + sensitive: Boolean, + previewListener: MediaPreviewListener, + showingContent: Boolean, + mediaPreviewHeight: Int) { + + val context = itemView.context + val mediaPreviews = arrayOf( + itemView.findViewById(R.id.status_media_preview_0), + itemView.findViewById(R.id.status_media_preview_1), + itemView.findViewById(R.id.status_media_preview_2), + itemView.findViewById(R.id.status_media_preview_3)) + + val mediaOverlays = arrayOf( + itemView.findViewById(R.id.status_media_overlay_0), + itemView.findViewById(R.id.status_media_overlay_1), + itemView.findViewById(R.id.status_media_overlay_2), + itemView.findViewById(R.id.status_media_overlay_3)) + + val sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning) + val sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button) + val mediaLabel = itemView.findViewById(R.id.status_media_label) + if (statusDisplayOptions.mediaPreviewEnabled) { + // Hide the unused label. + mediaLabel.visibility = View.GONE + } else { + setMediaLabel(mediaLabel, attachments, sensitive, previewListener) + // Hide all unused views. + mediaPreviews[0].visibility = View.GONE + mediaPreviews[1].visibility = View.GONE + mediaPreviews[2].visibility = View.GONE + mediaPreviews[3].visibility = View.GONE + sensitiveMediaWarning.visibility = View.GONE + sensitiveMediaShow.visibility = View.GONE + return + } + + + val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent)) + + val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS) + + for (i in 0 until n) { + val attachment = attachments[i] + val previewUrl = attachment.previewUrl + val description = attachment.description + + if (TextUtils.isEmpty(description)) { + mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media) + } else { + mediaPreviews[i].contentDescription = description + } + + mediaPreviews[i].visibility = View.VISIBLE + + if (TextUtils.isEmpty(previewUrl)) { + Glide.with(mediaPreviews[i]) + .load(mediaPreviewUnloaded) + .centerInside() + .into(mediaPreviews[i]) + } else { + val placeholder = if (attachment.blurhash != null) + decodeBlurHash(context, attachment.blurhash) + else mediaPreviewUnloaded + val meta = attachment.meta + val focus = meta?.focus + if (showingContent) { + if (focus != null) { // If there is a focal point for this attachment: + mediaPreviews[i].setFocalPoint(focus) + + Glide.with(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(mediaPreviews[i]) + .into(mediaPreviews[i]) + } else { + mediaPreviews[i].removeFocalPoint() + + Glide.with(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .into(mediaPreviews[i]) + } + } else { + mediaPreviews[i].removeFocalPoint() + if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) { + val blurhashBitmap = decodeBlurHash(context, attachment.blurhash) + mediaPreviews[i].setImageDrawable(blurhashBitmap) + } else { + mediaPreviews[i].setImageDrawable(mediaPreviewUnloaded) + } + } + } + + val type = attachment.type + if (showingContent + && (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) { + mediaOverlays[i].visibility = View.VISIBLE + } else { + mediaOverlays[i].visibility = View.GONE + } + + mediaPreviews[i].setOnClickListener { v -> + previewListener.onViewMedia(v, i) + } + + if (n <= 2) { + mediaPreviews[0].layoutParams.height = mediaPreviewHeight * 2 + mediaPreviews[1].layoutParams.height = mediaPreviewHeight * 2 + } else { + mediaPreviews[0].layoutParams.height = mediaPreviewHeight + mediaPreviews[1].layoutParams.height = mediaPreviewHeight + mediaPreviews[2].layoutParams.height = mediaPreviewHeight + mediaPreviews[3].layoutParams.height = mediaPreviewHeight + } + } + if (attachments.isNullOrEmpty()) { + sensitiveMediaWarning.visibility = View.GONE + sensitiveMediaShow.visibility = View.GONE + } else { + sensitiveMediaWarning.text = if (sensitive) { + context.getString(R.string.status_sensitive_media_title) + } else { + context.getString(R.string.status_media_hidden_title) + } + + sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE + sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE + sensitiveMediaShow.setOnClickListener { v -> + previewListener.onContentHiddenChange(false) + v.visibility = View.GONE + sensitiveMediaWarning.visibility = View.VISIBLE + setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener, + false, mediaPreviewHeight) + } + sensitiveMediaWarning.setOnClickListener { v -> + previewListener.onContentHiddenChange(true) + v.visibility = View.GONE + sensitiveMediaShow.visibility = View.VISIBLE + setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener, + true, mediaPreviewHeight) + } + } + + // Hide any of the placeholder previews beyond the ones set. + for (i in n until Status.MAX_MEDIA_ATTACHMENTS) { + mediaPreviews[i].visibility = View.GONE + } + } + + private fun setMediaLabel(mediaLabel: TextView, attachments: List, sensitive: Boolean, + listener: MediaPreviewListener) { + if (attachments.isEmpty()) { + mediaLabel.visibility = View.GONE + return + } + mediaLabel.visibility = View.VISIBLE + + // Set the label's text. + val context = mediaLabel.context + var labelText = getLabelTypeText(context, attachments[0].type) + if (sensitive) { + val sensitiveText = context.getString(R.string.status_sensitive_media_title) + labelText += String.format(" (%s)", sensitiveText) + } + mediaLabel.text = labelText + + // Set the icon next to the label. + val drawableId = getLabelIcon(attachments[0].type) + mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawableId, 0, 0, 0) + + mediaLabel.setOnClickListener { listener.onViewMedia(null, 0) } + } + + private fun getLabelTypeText(context: Context, type: Attachment.Type): String { + return when (type) { + Attachment.Type.IMAGE -> context.getString(R.string.status_media_images) + Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video) + Attachment.Type.AUDIO -> context.getString(R.string.status_media_audio) + else -> context.getString(R.string.status_media_attachments) + } + } + + @DrawableRes + private fun getLabelIcon(type: Attachment.Type): Int { + return when (type) { + Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp + Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp + Attachment.Type.AUDIO -> R.drawable.ic_music_box_24dp + else -> R.drawable.ic_attach_file_24dp + } + } + + fun setupPollReadonly(poll: PollViewData?, emojis: List, useAbsoluteTime: Boolean) { + val pollResults = listOf( + itemView.findViewById(R.id.status_poll_option_result_0), + itemView.findViewById(R.id.status_poll_option_result_1), + itemView.findViewById(R.id.status_poll_option_result_2), + itemView.findViewById(R.id.status_poll_option_result_3)) + + val pollDescription = itemView.findViewById(R.id.status_poll_description) + + if (poll == null) { + for (pollResult in pollResults) { + pollResult.visibility = View.GONE + } + pollDescription.visibility = View.GONE + } else { + val timestamp = System.currentTimeMillis() + + + setupPollResult(poll, emojis, pollResults) + + pollDescription.visibility = View.VISIBLE + pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime) + } + } + + private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence { + val context = pollDescription.context + val votesText = if(poll.votersCount == null) { + val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong()) + context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes) + } else { + val votes = NumberFormat.getNumberInstance().format(poll.votersCount.toLong()) + context.resources.getQuantityString(R.plurals.poll_info_people, poll.votersCount, votes) + } + val pollDurationInfo = if (poll.expired) { + context.getString(R.string.poll_info_closed) + } else { + if (useAbsoluteTime) { + context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt)) + } else { + TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) + } + } + + return context.getString(R.string.poll_info_format, votesText, pollDurationInfo) + } + + + private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List) { + val options = poll.options + + for (i in 0 until Status.MAX_POLL_OPTIONS) { + if (i < options.size) { + val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount) + + val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context) + pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i]) + pollResults[i].visibility = View.VISIBLE + + val level = percent * 100 + + pollResults[i].background.level = level + + } else { + pollResults[i].visibility = View.GONE + } + } + } + + fun getAbsoluteTime(time: Date?): String { + return if (time != null) { + if (android.text.format.DateUtils.isToday(time.time)) { + shortSdf.format(time) + } else { + longSdf.format(time) + } + } else { + "??:??:??" + } + } + + companion object { + val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter) + val NO_INPUT_FILTER = arrayOfNulls(0) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt new file mode 100644 index 0000000..83eaeaf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -0,0 +1,91 @@ +@file:JvmName("StringUtils") + +package com.keylesspalace.tusky.util + +import android.text.Spanned +import java.util.* + + +private const val POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +fun randomAlphanumericString(count: Int): String { + val chars = CharArray(count) + val random = Random() + for (i in 0 until count) { + chars[i] = POSSIBLE_CHARS[random.nextInt(POSSIBLE_CHARS.length)] + } + return String(chars) +} + +// We sort statuses by ID. Something we need to invent some ID for placeholder. +// Not sure if inc()/dec() should be made `operator` or not + +/** + * "Increment" string so that during sorting it's bigger than [this]. + */ +fun String.inc(): String { + // We assume that we will stay in the safe range for now + val builder = this.toCharArray() + builder[lastIndex] = builder[lastIndex].inc() + return String(builder) +} + + +/** + * "Decrement" string so that during sorting it's smaller than [this]. + */ +fun String.dec(): String { + if (this.isEmpty()) return this + + val builder = this.toCharArray() + var i = builder.lastIndex + while (i > 0) { + if (builder[i] > '0') { + builder[i] = builder[i].dec() + return String(builder) + } else { + builder[i] = 'z' + } + i-- + } + return if (builder[0] > '1') { + builder[0] = builder[0].dec() + String(builder) + } else { + String(builder.copyOfRange(1, builder.size)) + } +} + +/** + * A < B (strictly) by length and then by content. + * Examples: + * "abc" < "bcd" + * "ab" < "abc" + * "cb" < "abc" + * not: "ab" < "ab" + * not: "abc" > "cb" + */ +fun String.isLessThan(other: String): Boolean { + return when { + this.length < other.length -> true + this.length > other.length -> false + else -> this < other + } +} + +fun Spanned.trimTrailingWhitespace(): Spanned { + var i = length + do { + i-- + } while (i >= 0 && get(i).isWhitespace()) + return subSequence(0, i + 1) as Spanned +} + +/** + * BidiFormatter.unicodeWrap is insufficient in some cases (see #1921) + * So we force isolation manually + * https://unicode.org/reports/tr9/#Explicit_Directional_Isolates + */ +fun CharSequence.unicodeWrap(): String { + return "\u2068${this}\u2069" +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java new file mode 100644 index 0000000..8c04a7d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java @@ -0,0 +1,83 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDelegate; + +/** + * Provides runtime compatibility to obtain theme information and re-theme views, especially where + * the ability to do so is not supported in resource files. + */ +public class ThemeUtils { + + public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT; + + private static final String THEME_NIGHT = "night"; + private static final String THEME_DAY = "day"; + private static final String THEME_BLACK = "black"; + private static final String THEME_AUTO = "auto"; + private static final String THEME_SYSTEM = "auto_system"; + + @ColorInt + public static int getColor(@NonNull Context context, @AttrRes int attribute) { + TypedValue value = new TypedValue(); + if (context.getTheme().resolveAttribute(attribute, value, true)) { + return value.data; + } else { + return Color.BLACK; + } + } + + public static int getDimension(@NonNull Context context, @AttrRes int attribute) { + TypedArray array = context.obtainStyledAttributes(new int[] { attribute }); + int dimen = array.getDimensionPixelSize(0, -1); + array.recycle(); + return dimen; + } + + public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) { + drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN); + } + + public static void setAppNightMode(String flavor) { + switch (flavor) { + default: + case THEME_NIGHT: + case THEME_BLACK: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + break; + case THEME_DAY: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + break; + case THEME_AUTO: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME); + break; + case THEME_SYSTEM: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + break; + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java new file mode 100644 index 0000000..c94b422 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java @@ -0,0 +1,105 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import android.content.Context; + +import com.keylesspalace.tusky.R; + +public class TimestampUtils { + + private static final long SECOND_IN_MILLIS = 1000; + private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; + private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; + private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; + private static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365; + + /** + * This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString}, + * but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. + */ + public static String getRelativeTimeSpanString(Context context, long then, long now) { + long span = now - then; + boolean future = false; + if (span < 0) { + future = true; + span = -span; + } + int format; + if (span < MINUTE_IN_MILLIS) { + span /= SECOND_IN_MILLIS; + if (future) { + format = R.string.abbreviated_in_seconds; + } else { + format = R.string.abbreviated_seconds_ago; + } + } else if (span < HOUR_IN_MILLIS) { + span /= MINUTE_IN_MILLIS; + if (future) { + format = R.string.abbreviated_in_minutes; + } else { + format = R.string.abbreviated_minutes_ago; + } + } else if (span < DAY_IN_MILLIS) { + span /= HOUR_IN_MILLIS; + if (future) { + format = R.string.abbreviated_in_hours; + } else { + format = R.string.abbreviated_hours_ago; + } + } else if (span < YEAR_IN_MILLIS) { + span /= DAY_IN_MILLIS; + if (future) { + format = R.string.abbreviated_in_days; + } else { + format = R.string.abbreviated_days_ago; + } + } else { + span /= YEAR_IN_MILLIS; + if (future) { + format = R.string.abbreviated_in_years; + } else { + format = R.string.abbreviated_years_ago; + } + } + return context.getString(format, span); + } + + public static String formatPollDuration(Context context, long then, long now) { + long span = then - now; + if (span < 0) { + span = 0; + } + int format; + if (span < MINUTE_IN_MILLIS) { + span /= SECOND_IN_MILLIS; + format = R.plurals.poll_timespan_seconds; + } else if (span < HOUR_IN_MILLIS) { + span /= MINUTE_IN_MILLIS; + format = R.plurals.poll_timespan_minutes; + + } else if (span < DAY_IN_MILLIS) { + span /= HOUR_IN_MILLIS; + format = R.plurals.poll_timespan_hours; + + } else { + span /= DAY_IN_MILLIS; + format = R.plurals.poll_timespan_days; + } + return context.getResources().getQuantityString(format, (int) span, (int) span); + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java new file mode 100644 index 0000000..ef4801a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java @@ -0,0 +1,49 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import androidx.annotation.NonNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class VersionUtils { + + private int major; + private int minor; + private int patch; + private String versionString; + + public VersionUtils(@NonNull String versionString) { + this.versionString = versionString; + String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(versionString); + if (matcher.find()) { + major = Integer.parseInt(matcher.group(1)); + minor = Integer.parseInt(matcher.group(2)); + patch = Integer.parseInt(matcher.group(3)); + } + } + + public boolean supportsScheduledToots() { + return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2); + } + + public boolean isPleroma() { + return versionString.contains(" (compatible; Pleroma "); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java new file mode 100644 index 0000000..abcd8d8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -0,0 +1,128 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.Chat; +import com.keylesspalace.tusky.entity.ChatMessage; +import com.keylesspalace.tusky.viewdata.NotificationViewData; +import com.keylesspalace.tusky.viewdata.StatusViewData; +import com.keylesspalace.tusky.viewdata.ChatViewData; +import com.keylesspalace.tusky.viewdata.ChatMessageViewData; + +/** + * Created by charlag on 12/07/2017. + */ + +public final class ViewDataUtils { + @Nullable + public static StatusViewData.Concrete statusToViewData(@Nullable Status status, + boolean alwaysShowSensitiveMedia, + boolean alwaysOpenSpoiler) { + if (status == null) return null; + Status visibleStatus = status.getReblog() == null ? status : status.getReblog(); + return new StatusViewData.Builder().setId(status.getId()) + .setAttachments(visibleStatus.getAttachments()) + .setAvatar(visibleStatus.getAccount().getAvatar()) + .setContent(visibleStatus.getContent()) + .setCreatedAt(visibleStatus.getCreatedAt()) + .setReblogsCount(visibleStatus.getReblogsCount()) + .setFavouritesCount(visibleStatus.getFavouritesCount()) + .setInReplyToId(visibleStatus.getInReplyToId()) + .setInReplyToAccountAcct(visibleStatus.getInReplyToAccountAcct()) + .setFavourited(visibleStatus.getFavourited()) + .setBookmarked(visibleStatus.getBookmarked()) + .setReblogged(visibleStatus.getReblogged()) + .setIsExpanded(alwaysOpenSpoiler) + .setIsShowingSensitiveContent(false) + .setMentions(visibleStatus.getMentions()) + .setNickname(visibleStatus.getAccount().getUsername()) + .setRebloggedAvatar(status.getReblog() == null ? null : status.getAccount().getAvatar()) + .setSensitive(visibleStatus.getSensitive()) + .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) + .setSpoilerText(visibleStatus.getSpoilerText()) + .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) + .setUserFullName(visibleStatus.getAccount().getName()) + .setVisibility(visibleStatus.getVisibility()) + .setSenderId(visibleStatus.getAccount().getId()) + .setRebloggingEnabled(visibleStatus.rebloggingAllowed()) + .setApplication(visibleStatus.getApplication()) + .setStatusEmojis(visibleStatus.getEmojis()) + .setAccountEmojis(visibleStatus.getAccount().getEmojis()) + .setRebloggedByEmojis(status.getReblog() == null ? null : status.getAccount().getEmojis()) + .setCollapsible(SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) + .setCollapsed(true) + .setPoll(visibleStatus.getPoll()) + .setCard(visibleStatus.getCard()) + .setIsBot(visibleStatus.getAccount().getBot()) + .setMuted(visibleStatus.isMuted()) + .setUserMuted(visibleStatus.isUserMuted()) + .setThreadMuted(visibleStatus.isThreadMuted()) + .setConversationId(visibleStatus.getConversationId()) + .setEmojiReactions(visibleStatus.getEmojiReactions()) + .setParentVisible(visibleStatus.getParentVisible()) + .createStatusViewData(); + } + + public static NotificationViewData.Concrete notificationToViewData(Notification notification, + boolean alwaysShowSensitiveData, + boolean alwaysOpenSpoiler) { + return new NotificationViewData.Concrete( + notification.getType(), + notification.getId(), + notification.getAccount(), + statusToViewData( + notification.getStatus(), + alwaysShowSensitiveData, + alwaysOpenSpoiler + ), + notification.getEmoji(), + notification.getTarget() + ); + } + + public static ChatMessageViewData.Concrete chatMessageToViewData(@Nullable ChatMessage msg) { + if(msg == null) return null; + + return new ChatMessageViewData.Concrete( + msg.getId(), + msg.getContent(), + msg.getChatId(), + msg.getAccountId(), + msg.getCreatedAt(), + msg.getAttachment(), + msg.getEmojis(), + msg.getCard() + ); + } + + @NonNull + public static ChatViewData.Concrete chatToViewData(Chat chat) { + return new ChatViewData.Concrete( + chat.getAccount(), + chat.getId(), + chat.getUnread(), + chatMessageToViewData( + chat.getLastMessage() + ), + chat.getUpdatedAt() + ); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt new file mode 100644 index 0000000..ae9a885 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt @@ -0,0 +1,115 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky.util + +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.EditText +import android.util.TypedValue +import android.content.res.Resources +import android.view.TouchDelegate +import android.view.MotionEvent +import android.graphics.Rect +/*import java.util.List +import java.util.ArrayList*/ + +fun View.show() { + this.visibility = View.VISIBLE +} + +fun View.hide() { + this.visibility = View.GONE +} + +fun View.visible(visible: Boolean, or: Int = View.GONE) { + this.visibility = if (visible) View.VISIBLE else or +} + +class MultipleTouchDelegate : TouchDelegate { + + var delegates = mutableListOf() + + constructor(v: View) : super(Rect(), v) + + public fun addDelegate(delegate: TouchDelegate) { + delegates.add(delegate) + } + + override fun onTouchEvent(event: MotionEvent) : Boolean { + var ret = false + val x = event.x + val y = event.y + + for(delegate in delegates) { + event.setLocation(x, y) + ret = delegate.onTouchEvent(event) || ret + } + + return ret + } +} + +fun View.increaseHitArea(vdp: Float, hdp: Float) { + val parent = this.parent as View + val vpixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, vdp, Resources.getSystem().displayMetrics).toInt() + val hpixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, hdp, Resources.getSystem().displayMetrics).toInt() + parent.post { + val rect = Rect() + this.getHitRect(rect) + rect.top -= vpixels + rect.left -= hpixels + rect.bottom += vpixels + rect.right += hpixels + if(parent.touchDelegate != null && parent.touchDelegate is MultipleTouchDelegate) { + (parent.touchDelegate as MultipleTouchDelegate).addDelegate(TouchDelegate(rect, this)) + } else { + val mtd = MultipleTouchDelegate(this) + mtd.addDelegate(TouchDelegate(rect, this)) + parent.touchDelegate = mtd + } + } +} + +open class DefaultTextWatcher : TextWatcher { + override fun afterTextChanged(s: Editable) { + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + } +} + +inline fun EditText.onTextChanged( + crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit) { + addTextChangedListener(object : DefaultTextWatcher() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + callback(s, start, before, count) + } + }) +} + +inline fun EditText.afterTextChanged( + crossinline callback: (s: Editable) -> Unit) { + addTextChangedListener(object : DefaultTextWatcher() { + override fun afterTextChanged(s: Editable) { + callback(s) + } + }) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java new file mode 100644 index 0000000..4698f44 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java @@ -0,0 +1,40 @@ +package com.keylesspalace.tusky.util; + +import androidx.viewpager2.widget.ViewPager2; +import androidx.recyclerview.widget.RecyclerView; +import java.lang.reflect.*; +import java.lang.*; + +/** + * ViewPager2 written by monkeys! + */ +public class ViewPager2Fix { + /** + * Thanks to @al.e.shevelev@medium.com for solution + */ + public static Field getViewPagerRecyclerViewField() throws NoSuchFieldException { + Field f = ViewPager2.class.getDeclaredField("mRecyclerView"); + f.setAccessible(true); + return f; + } + + public static Field getRecyclerViewTouchSlopField() throws NoSuchFieldException { + Field f = RecyclerView.class.getDeclaredField("mTouchSlop"); + f.setAccessible(true); + return f; + } + + public static void reduceVelocity(ViewPager2 pager, float val) { + try { + Field recyclerViewField = getViewPagerRecyclerViewField(); + Field touchSlopField = getRecyclerViewTouchSlopField(); + + RecyclerView recyclerView = (RecyclerView)recyclerViewField.get(pager); + int touchSlop = (int)touchSlopField.get(recyclerView); + touchSlopField.setInt(recyclerView, (int)(touchSlop*val)); + } catch(Exception e) { + // all possible exceptions must be caught during tests + ; + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt b/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt new file mode 100644 index 0000000..b003cb2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt @@ -0,0 +1,23 @@ +package com.keylesspalace.tusky.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String { + return PagingRequestHelper.RequestType.values().mapNotNull { + report.getErrorFor(it)?.message + }.first() +} + +fun PagingRequestHelper.createStatusLiveData(): LiveData { + val liveData = MutableLiveData() + addListener { report -> + when { + report.hasRunning() -> liveData.postValue(NetworkState.LOADING) + report.hasError() -> liveData.postValue( + NetworkState.error(getErrorMessage(report))) + else -> liveData.postValue(NetworkState.LOADED) + } + } + return liveData +} \ 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 new file mode 100644 index 0000000..4789ac3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +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.util.visible +import kotlinx.android.synthetic.main.view_background_message.view.* + + +/** + * This view is used for screens with downloadable content which may fail. + * Can show an image, text and button below them. + */ +class BackgroundMessageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + init { + View.inflate(context, R.layout.view_background_message, this) + gravity = Gravity.CENTER_HORIZONTAL + orientation = VERTICAL + + if (isInEditMode) { + setup(R.drawable.elephant_offline, R.string.error_network) {} + } + } + + /** + * 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java b/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java new file mode 100644 index 0000000..c31b37e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java @@ -0,0 +1,61 @@ +/* Copyright 2019 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.view; + +import android.content.Context; +import android.graphics.Outline; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; + +/** + * override BezelImageView from MaterialDrawer library to provide custom outline + */ + +public class BezelImageView extends com.mikepenz.materialdrawer.view.BezelImageView { + public BezelImageView(Context context) { + this(context, null); + } + + public BezelImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BezelImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onSizeChanged(int w, int h, int old_w, int old_h) { + setOutlineProvider(new CustomOutline(w, h)); + } + + private static class CustomOutline extends ViewOutlineProvider { + + int width; + int height; + + CustomOutline(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, width, height, width < height ? width / 8f : height / 8f); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt new file mode 100644 index 0000000..4011d69 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt @@ -0,0 +1,73 @@ +/* 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.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import androidx.recyclerview.widget.RecyclerView +import android.view.View +import androidx.core.content.ContextCompat + +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.ThreadAdapter + +class ConversationLineItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() { + + private val divider: Drawable = ContextCompat.getDrawable(context, R.drawable.conversation_thread_line)!! + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val dividerStart = parent.paddingStart + context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start) + val dividerEnd = dividerStart + divider.intrinsicWidth + + val childCount = parent.childCount + val avatarMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin) + + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + + val position = parent.getChildAdapterPosition(child) + val adapter = parent.adapter as ThreadAdapter + + val current = adapter.getItem(position) + val dividerTop: Int + val dividerBottom: Int + if (current != null) { + val above = adapter.getItem(position - 1) + dividerTop = if (above != null && above.id == current.inReplyToId) { + child.top + } else { + child.top + avatarMargin + } + val below = adapter.getItem(position + 1) + dividerBottom = if (below != null && current.id == below.inReplyToId && + adapter.detailedStatusPosition != position) { + child.bottom + } else { + child.top + avatarMargin + } + + if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + divider.setBounds(dividerStart, dividerTop, dividerEnd, dividerBottom) + } else { + divider.setBounds(canvas.width - dividerEnd, dividerTop, canvas.width - dividerStart, dividerBottom) + } + divider.draw(canvas) + + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/CustomEmojiTextView.kt b/app/src/main/java/com/keylesspalace/tusky/view/CustomEmojiTextView.kt new file mode 100644 index 0000000..d3a8515 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/CustomEmojiTextView.kt @@ -0,0 +1,60 @@ +package com.keylesspalace.tusky.view + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.text.Layout +import android.text.Spannable +import android.util.AttributeSet +import android.util.Log +import androidx.emoji.widget.EmojiAppCompatTextView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.EmojiSpan +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.emojify + +/* + * This is a TextView that changes break strategy to simple + * if there is too much custom emojis + * + * Fixes Android performance bug + */ + +class CustomEmojiTextView +@JvmOverloads constructor(context:Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +): EmojiAppCompatTextView(context, attrs, defStyleAttr) { + private var oldBreakStrategy = 1 // Layout.BREAK_STRATEGY_HIGH_QUALITY + + @SuppressLint("WrongConstant") + override fun setText(text: CharSequence?, type: BufferType?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + var overridden = false + + // don't change if break strategy already simple + if(text is Spannable && breakStrategy != Layout.BREAK_STRATEGY_SIMPLE) { + val spans = text.getSpans(0, text.length, EmojiSpan::class.java) + + if (spans.size >= SPAN_LIMIT) { + oldBreakStrategy = breakStrategy + breakStrategy = Layout.BREAK_STRATEGY_SIMPLE + overridden = true + + Log.d("CustomEmojiTextView", "break strategy overriden!"); + } + } + + if(!overridden) + breakStrategy = oldBreakStrategy + } + + super.setText(text, type) + } + + companion object { + const val SPAN_LIMIT = 100 // heuristics + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java b/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java new file mode 100644 index 0000000..501e02c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java @@ -0,0 +1,153 @@ +package com.keylesspalace.tusky.view; + +import android.view.*; +import android.content.*; +import android.util.*; +import android.widget.*; +import android.app.*; +import android.text.*; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import androidx.annotation.NonNull; +import androidx.viewpager2.widget.ViewPager2; +import androidx.recyclerview.widget.RecyclerView; +import androidx.preference.PreferenceManager; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.StickerAdapter; +import com.keylesspalace.tusky.adapter.UnicodeEmojiAdapter; +import com.keylesspalace.tusky.entity.StickerPack; + +import java.util.*; + +public class EmojiKeyboard extends LinearLayout { + private TabLayout tabs; + private ViewPager2 pager; + private TabLayoutMediator currentMediator; + private String preferenceKey; + private SharedPreferences pref; + private Set recents; + private String RECENTS_DELIM = "; "; + private int MAX_RECENTS_ITEMS = 50; + private RecyclerView.Adapter adapter; + public boolean isSticky = false; // TODO + + public EmojiKeyboard(Context context) { + super(context); + init(context); + } + + public EmojiKeyboard(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public EmojiKeyboard(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + void init(Context context) { + inflate(context, R.layout.item_emoji_picker, this); + + pref = PreferenceManager.getDefaultSharedPreferences(context); + tabs = findViewById(R.id.picker_tabs); + pager = findViewById(R.id.picker_pager); + } + + public static final int UNICODE_MODE = 0; + public static final int CUSTOM_MODE = 1; + public static final int STICKER_MODE = 2; + + private void setupKeyboardWithAdapter(RecyclerView.Adapter adapter, String preferenceKey) { + this.preferenceKey = preferenceKey; + this.adapter = adapter; + + List list = Arrays.asList(pref.getString(preferenceKey, "").split(RECENTS_DELIM)); + recents = new LinkedHashSet(list); + ((EmojiKeyboardAdapter)adapter).onRecentsUpdate(recents); + + pager.setAdapter(adapter); + + if(currentMediator != null) + currentMediator.detach(); + + currentMediator = new TabLayoutMediator(tabs, pager, (TabLayoutMediator.TabConfigurationStrategy)adapter); + currentMediator.attach(); + } + + public void setupStickerKeyboard(OnEmojiSelectedListener listener, StickerPack packs[]) { + MAX_RECENTS_ITEMS = 20; + setupKeyboardWithAdapter(new StickerAdapter(packs, (_id, _emoji) -> { + this.appendToRecents(_emoji); + listener.onEmojiSelected(_id, _emoji); + }), "STICKER_RECENTS"); + } + + public void setupKeyboard(String id, int mode, OnEmojiSelectedListener listener) { + switch(mode) { + // WOOOPS, I forgot that I need to pass data to adapter + // For stickers, use SetupStickerKeyboard instead + // For custom emoji, use TODO + case CUSTOM_MODE: + case STICKER_MODE: + throw new IllegalArgumentException(); + default: + case UNICODE_MODE: + setupKeyboardWithAdapter(new UnicodeEmojiAdapter(id, (_id, _emoji) -> { + this.appendToRecents(_emoji); + listener.onEmojiSelected(_id, _emoji); + }), "UNICODE_RECENTS"); + } + } + + private void appendToRecents(String id) { + recents.remove(id); + recents.add(id); + int size = recents.size(); + String joined; + final SharedPreferences.Editor editor = pref.edit(); + if(size > MAX_RECENTS_ITEMS) { + List list = new ArrayList(recents); + list = list.subList(size - MAX_RECENTS_ITEMS, size); + joined = TextUtils.join(RECENTS_DELIM, list); + if(isSticky) { + recents = new LinkedHashSet(list); + } + } else { + joined = TextUtils.join(RECENTS_DELIM, recents); + } + + editor.putString(preferenceKey, joined); + editor.apply(); + + // no point on updating view if we are will be closed + if(isSticky) { + ((EmojiKeyboardAdapter)adapter).onRecentsUpdate(recents); + } + } + + public interface OnEmojiSelectedListener { + void onEmojiSelected(@NonNull String id, @NonNull String emoji); + } + + public interface EmojiKeyboardAdapter { + void onRecentsUpdate(@NonNull Set set); + } + + public static void show(Context ctx, String id, int mode, OnEmojiSelectedListener listener) { + final Dialog dialog = new Dialog(ctx); + + dialog.setTitle(null); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setContentView(R.layout.dialog_emoji_keyboard); + EmojiKeyboard kbd = (EmojiKeyboard)dialog.findViewById(R.id.dialog_emoji_keyboard); + kbd.setupKeyboard(id, mode, (_id, _emoji) -> { + listener.onEmojiSelected(_id, _emoji); + if(!kbd.isSticky) + dialog.dismiss(); + }); + + dialog.show(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt new file mode 100644 index 0000000..09e648a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class EmojiPicker @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : RecyclerView(context, attrs) { + + init { + clipToPadding = false + layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java b/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java new file mode 100644 index 0000000..50f9ea6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java @@ -0,0 +1,54 @@ +/* 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.view; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener { + private static final int VISIBLE_THRESHOLD = 15; + private int previousTotalItemCount; + private LinearLayoutManager layoutManager; + + public EndlessOnScrollListener(LinearLayoutManager layoutManager) { + this.layoutManager = layoutManager; + previousTotalItemCount = 0; + } + + @Override + public void onScrolled(@NonNull RecyclerView view, int dx, int dy) { + int totalItemCount = layoutManager.getItemCount(); + int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); + if (totalItemCount < previousTotalItemCount) { + previousTotalItemCount = totalItemCount; + + } + if (totalItemCount != previousTotalItemCount) { + previousTotalItemCount = totalItemCount; + } + + if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) { + onLoadMore(totalItemCount, view); + } + } + + public void reset() { + previousTotalItemCount = 0; + } + + public abstract void onLoadMore(int totalItemsCount, RecyclerView view); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt new file mode 100644 index 0000000..ec748e0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt @@ -0,0 +1,33 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.VideoView + +class ExposedPlayPauseVideoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : VideoView(context, attrs, defStyleAttr) { + + private var listener: PlayPauseListener? = null + + fun setPlayPauseListener(listener: PlayPauseListener) { + this.listener = listener + } + + override fun start() { + super.start() + listener?.onPlay() + } + + override fun pause() { + super.pause() + listener?.onPause() + } + + interface PlayPauseListener { + fun onPlay() + fun onPause() + } +} \ 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 new file mode 100644 index 0000000..2c73cd5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -0,0 +1,58 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.card.MaterialCardView +import com.keylesspalace.tusky.R +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( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attrs, defStyleAttr) { + + init { + inflate(context, R.layout.card_license, this) + + setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) + + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LicenseCard, 0, 0) + + val name: String? = a.getString(R.styleable.LicenseCard_name) + val license: String? = a.getString(R.styleable.LicenseCard_license) + val link: String? = a.getString(R.styleable.LicenseCard_link) + a.recycle() + + licenseCardName.text = name + licenseCardLicense.text = license + if(link.isNullOrBlank()) { + licenseCardLink.hide() + } else { + licenseCardLink.text = link + setOnClickListener { LinkHelper.openLink(link, context) } + } + + } + +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt new file mode 100644 index 0000000..42bfc27 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt @@ -0,0 +1,129 @@ +/* Copyright 2018 Jochem Raat + * + * 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.view + +import android.content.Context +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.keylesspalace.tusky.entity.Attachment + +import com.keylesspalace.tusky.util.FocalPointUtil + +/** + * This is an extension of the standard android ImageView, which makes sure to update the custom + * matrix when its size changes if a focal point is set. + * + * If a focal point is set on this view, it will use the FocalPointUtil to update the image + * matrix each time the size of the view is changed. This is needed to ensure that the correct + * cropping is maintained. + * + * However if there is no focal point set (e.g. it is null), then this view should simply + * act exactly the same as an ordinary android ImageView. + */ +class MediaPreviewImageView +@JvmOverloads constructor( +context: Context, +attrs: AttributeSet? = null, +defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr),RequestListener { + private var focus: Attachment.Focus? = null + private var focalMatrix: Matrix? = null + + /** + * Set the focal point for this view. + */ + fun setFocalPoint(focus: Attachment.Focus?) { + this.focus = focus + super.setScaleType(ScaleType.MATRIX) + + if (focalMatrix == null) { + focalMatrix = Matrix() + } + } + + /** + * Remove the focal point from this view (if there was one). + */ + fun removeFocalPoint() { + super.setScaleType(ScaleType.CENTER_CROP) + focus = null + } + + /** + * Overridden getScaleType method which returns CENTER_CROP if we have a focal point set. + * + * This is necessary because the Android transitions framework tries to copy the scale type + * from this view to the PhotoView when animating between this view and the detailled view of + * the image. Since the PhotoView does not support a MATRIX scale type, the app would crash + * if we simply passed that on, so instead we pretend that CENTER_CROP is still used here + * even if we have a focus point set. + */ + override fun getScaleType(): ScaleType { + return if (focus != null) { + ScaleType.CENTER_CROP + } else { + super.getScaleType() + } + } + + /** + * Overridden setScaleType method which only accepts the new type if we don't have a focal + * point set. + * + */ + override fun setScaleType(type: ScaleType) { + if (focus != null) { + super.setScaleType(ScaleType.MATRIX) + } else { + super.setScaleType(type) + } + } + + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + return false + } + + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + recalculateMatrix(width, height, resource) + return false + } + + + /** + * Called when the size of the view changes, it calls the FocalPointUtil to update the + * matrix if we have a set focal point. It then reassigns the matrix to this imageView. + */ + override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { + recalculateMatrix(width, height, drawable) + + super.onSizeChanged(width, height, oldWidth, oldHeight) + } + + private fun recalculateMatrix(width: Int, height: Int, drawable: Drawable?) { + if (drawable != null && focus != null && focalMatrix != null) { + scaleType = ScaleType.MATRIX + FocalPointUtil.updateFocalPointMatrix(width.toFloat(), height.toFloat(), + drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat(), + focus as Attachment.Focus, focalMatrix as Matrix) + imageMatrix = focalMatrix + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt new file mode 100644 index 0000000..2cf8ad6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -0,0 +1,32 @@ +@file:JvmName("MuteAccountDialog") + +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 + +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.setChecked(true) + + AlertDialog.Builder(activity) + .setView(view) + .setPositiveButton(android.R.string.ok) { _, _ -> + val spinner: Spinner = view.findViewById(R.id.duration) + val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) + onOk(checkbox.isChecked, durationValues[spinner.selectedItemPosition]) + } + .setNegativeButton(android.R.string.cancel, null) + .show() +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt new file mode 100644 index 0000000..d0e7305 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt @@ -0,0 +1,24 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import androidx.appcompat.widget.AppCompatImageView +import android.util.AttributeSet + +/** + * Created by charlag on 26/10/2017. + */ + +class SquareImageView : AppCompatImageView { + constructor(context: Context) : super(context) + + constructor(context: Context, attributes: AttributeSet) : super(context, attributes) + + constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) + : super(context, attributes, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = measuredWidth + setMeasuredDimension(width, width) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt b/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt new file mode 100644 index 0000000..0b3d1a4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt @@ -0,0 +1,75 @@ +package com.keylesspalace.tusky.view + +import android.view.* +import android.content.* +import android.util.* +import android.widget.* +import android.app.* +import android.text.* +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator + +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.preference.PreferenceManager + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ViewDataUtils +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys + +import java.util.*; + +class StatusView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : ConstraintLayout(context, attrs, defStyleAttr) { + + private var viewHolder : StatusViewHolder + private var statusDisplayOptions : StatusDisplayOptions + init { + View.inflate(context, R.layout.item_status, this) + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = false, + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + renderStatusAsMention = preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + hideStats = false + ) + viewHolder = StatusViewHolder(this) + } + + fun setupWithStatus(status: Status) { + val concrete = ViewDataUtils.statusToViewData(status, false, false) + viewHolder.setupWithStatus(concrete, DummyStatusActionListener(), statusDisplayOptions) + } + + class DummyStatusActionListener: StatusActionListener { + override fun onReply(position: Int) { } + override fun onReblog(reblog: Boolean, position: Int) { } + override fun onFavourite(favourite: Boolean, position: Int) { } + override fun onBookmark(bookmark: Boolean, position: Int) { } + override fun onMore(view: View, position: Int) { } + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { } + override fun onViewThread(position: Int) { } + override fun onViewReplyTo(position: Int) { } + override fun onOpenReblog(position: Int) { } + override fun onExpandedChange(expanded: Boolean, position: Int) { } + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { } + override fun onLoadMore(position: Int) { } + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { } + override fun onVoteInPoll(position: Int, choices: MutableList) { } + override fun onViewAccount(id: String) { } + override fun onViewTag(id: String) { } + override fun onViewUrl(id: String) { } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt new file mode 100644 index 0000000..6bf7103 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -0,0 +1,30 @@ +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 + +@Parcelize +data class AttachmentViewData( + val attachment: Attachment, + val statusId: String?, + val statusUrl: String? +) : Parcelable { + companion object { + @JvmStatic + fun list(status: Status): List { + val actionable = status.actionableStatus + return actionable.attachments.map { + AttachmentViewData(it, actionable.id, actionable.url!!) + } + } + + fun list(attachments: List): List { + return attachments.map { + AttachmentViewData(it, it.id, it.url) + } + } + + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt new file mode 100644 index 0000000..2ebbb61 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt @@ -0,0 +1,135 @@ +package com.keylesspalace.tusky.viewdata + +import android.text.Spanned +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card +import com.keylesspalace.tusky.entity.Emoji +import java.util.* + + +abstract class ChatViewData { + abstract fun getViewDataId() : Int + abstract fun deepEquals(o: ChatViewData) : Boolean + + class Concrete(val account : Account, + val id: String, + val unread: Long, + val lastMessage: ChatMessageViewData.Concrete?, + val updatedAt: Date ) : ChatViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatViewData): Boolean { + if (o !is Concrete) return false + return Objects.equals(o.account, account) + && Objects.equals(o.id, id) + && o.unread == unread + && (lastMessage == o.lastMessage || (lastMessage != null && o.lastMessage != null && o.lastMessage.deepEquals(lastMessage))) + && Objects.equals(o.updatedAt, updatedAt) + } + + override fun hashCode(): Int { + return Objects.hash(account, id, unread, lastMessage, updatedAt) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Concrete) + } + } + + class Placeholder(val id: String, val isLoading: Boolean) : ChatViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatViewData): Boolean { + if( o !is Placeholder ) return false + return o.isLoading == isLoading && o.id == id + } + + override fun hashCode(): Int { + var result = if (isLoading) 1 else 0 + result = 31 * result + id.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Placeholder) + } + } +} + +abstract class ChatMessageViewData { + abstract fun getViewDataId() : Int + abstract fun deepEquals(o: ChatMessageViewData) : Boolean + + class Concrete(val id: String, + val content: Spanned?, + val chatId: String, + val accountId: String, + val createdAt: Date, + val attachment: Attachment?, + val emojis: List, + val card: Card?) : ChatMessageViewData() + { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatMessageViewData): Boolean { + if( o !is Concrete ) return false + + return Objects.equals(o.id, id) + && Objects.equals(o.content, content) + && Objects.equals(o.chatId, chatId) + && Objects.equals(o.accountId, accountId) + && Objects.equals(o.createdAt, createdAt) + && Objects.equals(o.attachment, attachment) + && Objects.equals(o.emojis, emojis) + && Objects.equals(o.card, card) + } + + override fun hashCode() : Int { + return Objects.hash(id, content, chatId, accountId, createdAt, attachment, card) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Concrete) + } + } + + class Placeholder(val id: String, val isLoading: Boolean) : ChatMessageViewData() { + override fun getViewDataId(): Int { + return id.hashCode() + } + + override fun deepEquals(o: ChatMessageViewData): Boolean { + if( o !is Placeholder) return false + return o.isLoading == isLoading && o.id == id + } + + override fun hashCode(): Int { + var result = if (isLoading) 1 else 0 + result = 31 * result + id.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return deepEquals(other as Placeholder) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java new file mode 100644 index 0000000..845ecc2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -0,0 +1,144 @@ +/* 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.viewdata; + +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.Notification; + +import java.util.Objects; + +import io.reactivex.annotations.Nullable; + +/** + * Created by charlag on 12/07/2017. + *

+ * Class to represent data required to display either a notification or a placeholder. + * It is either a {@link Placeholder} or a {@link Concrete}. + * It is modelled this way because close relationship between placeholder and concrete notification + * is fine in this case. Placeholder case is not modelled as a type of notification because + * invariants would be violated and because it would model domain incorrectly. It is prefereable to + * {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and + * more native. + */ +public abstract class NotificationViewData { + private NotificationViewData() { + } + + public abstract long getViewDataId(); + + public abstract boolean deepEquals(NotificationViewData other); + + public static final class Concrete extends NotificationViewData { + private final Notification.Type type; + private final String id; + private final Account account; + @Nullable + private final StatusViewData.Concrete statusViewData; + @Nullable + private final String emoji; + @Nullable + private final Account target; // move notification + + public Concrete(Notification.Type type, String id, Account account, + @Nullable StatusViewData.Concrete statusViewData, + @Nullable String emoji, @Nullable Account target) { + this.type = type; + this.id = id; + this.account = account; + this.statusViewData = statusViewData; + this.emoji = emoji; + this.target = target; + } + + public Notification.Type getType() { + return type; + } + + public String getId() { + return id; + } + + public Account getAccount() { + return account; + } + + @Nullable + public StatusViewData.Concrete getStatusViewData() { + return statusViewData; + } + + @Nullable + public String getEmoji() { + return emoji; + } + + @Nullable + public Account getTarget() { + return target; + } + + @Override + public long getViewDataId() { + return id.hashCode(); + } + + @Override + public boolean deepEquals(NotificationViewData o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Concrete concrete = (Concrete) o; + return type == concrete.type && + Objects.equals(id, concrete.id) && + account.getId().equals(concrete.account.getId()) && + (emoji != null && concrete.emoji != null && emoji.equals(concrete.emoji)) && + (target != null && concrete.target != null && target.getId().equals(concrete.target.getId())) && + (statusViewData == concrete.statusViewData || + statusViewData != null && + statusViewData.deepEquals(concrete.statusViewData)); + } + + @Override + public int hashCode() { + return Objects.hash(type, id, account, statusViewData); + } + } + + public static final class Placeholder extends NotificationViewData { + private final long id; + private final boolean isLoading; + + public Placeholder(long id, boolean isLoading) { + this.id = id; + this.isLoading = isLoading; + } + + public boolean isLoading() { + return isLoading; + } + + @Override + public long getViewDataId() { + return id; + } + + @Override + public boolean deepEquals(NotificationViewData other) { + if (!(other instanceof Placeholder)) return false; + Placeholder that = (Placeholder) other; + return isLoading == that.isLoading && id == that.id; + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt new file mode 100644 index 0000000..f0ca626 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt @@ -0,0 +1,80 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.viewdata + +import android.content.Context +import android.text.SpannableStringBuilder +import android.text.Spanned +import androidx.core.text.parseAsHtml +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.PollOption +import java.util.* +import kotlin.math.roundToInt + +data class PollViewData( + val id: String, + val expiresAt: Date?, + val expired: Boolean, + val multiple: Boolean, + val votesCount: Int, + val votersCount: Int?, + val options: List, + var voted: Boolean +) + +data class PollOptionViewData( + val title: String, + var votesCount: Int, + var selected: Boolean +) + +fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int { + return if (fraction == 0) { + 0 + } else { + val total = totalVoters ?: totalVotes + (fraction / total.toDouble() * 100).roundToInt() + } +} + +fun buildDescription(title: String, percent: Int, context: Context): Spanned { + return SpannableStringBuilder(context.getString(R.string.poll_percent_format, percent).parseAsHtml()) + .append(" ") + .append(title) +} + +fun Poll?.toViewData(): PollViewData? { + if (this == null) return null + return PollViewData( + id, + expiresAt, + expired, + multiple, + votesCount, + votersCount, + options.map { it.toViewData() }, + voted + ) +} + +fun PollOption.toViewData(): PollOptionViewData { + return PollOptionViewData( + title, + votesCount, + false + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java new file mode 100644 index 0000000..b9126b8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -0,0 +1,765 @@ +/* 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.viewdata; + +import android.os.Build; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import androidx.annotation.Nullable; + +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Card; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.entity.Poll; +import com.keylesspalace.tusky.entity.Status; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * Created by charlag on 11/07/2017. + *

+ * Class to represent data required to display either a notification or a placeholder. + * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. + */ + +public abstract class StatusViewData { + + private StatusViewData() { } + + public abstract long getViewDataId(); + + public abstract boolean deepEquals(StatusViewData other); + + public static final class Concrete extends StatusViewData { + private static final char SOFT_HYPHEN = '\u00ad'; + private static final char ASCII_HYPHEN = '-'; + + private final String id; + private final Spanned content; + final boolean reblogged; + final boolean favourited; + final boolean bookmarked; + @Nullable + private final String spoilerText; + private final Status.Visibility visibility; + private final List attachments; + @Nullable + private final String rebloggedByUsername; + @Nullable + private final String rebloggedAvatar; + private final boolean isSensitive; + final boolean isExpanded; + private final boolean isShowingContent; + private final String userFullName; + private final String nickname; + private final String avatar; + private final Date createdAt; + private final int reblogsCount; + private final int favouritesCount; + @Nullable + private final String inReplyToId; + @Nullable + private final String inReplyToAccountAcct; + // I would rather have something else but it would be too much of a rewrite + @Nullable + private final Status.Mention[] mentions; + private final String senderId; + private final boolean rebloggingEnabled; + private final Status.Application application; + private final List statusEmojis; + private final List accountEmojis; + private final List rebloggedByAccountEmojis; + @Nullable + private final Card card; + private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */ + final boolean isCollapsed; /** Whether the status is shown partially or fully */ + @Nullable + private final PollViewData poll; + private final boolean isBot; + private final boolean isMuted; /* user toggle */ + private final boolean isThreadMuted; /* thread_muted state got from backend */ + private final boolean isUserMuted; /* muted state got from backend */ + private final int conversationId; + @Nullable + private final List emojiReactions; + private final boolean parentVisible; + + public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, + @Nullable String spoilerText, Status.Visibility visibility, List attachments, + @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, + boolean isShowingContent, String userFullName, String nickname, String avatar, + Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, + @Nullable String inReplyToAccountAcct, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, + Status.Application application, List statusEmojis, List accountEmojis, List rebloggedByAccountEmojis, @Nullable Card card, + boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isMuted, boolean isThreadMuted, + boolean isUserMuted, int conversationId, @Nullable List emojiReactions, boolean parentVisible) { + + this.id = id; + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { + // https://github.com/tuskyapp/Tusky/issues/563 + this.content = replaceCrashingCharacters(content); + this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString(); + this.nickname = replaceCrashingCharacters(nickname).toString(); + } else { + this.content = content; + this.spoilerText = spoilerText; + this.nickname = nickname; + } + this.reblogged = reblogged; + this.favourited = favourited; + this.bookmarked = bookmarked; + this.visibility = visibility; + this.attachments = attachments; + this.rebloggedByUsername = rebloggedByUsername; + this.rebloggedAvatar = rebloggedAvatar; + this.isSensitive = sensitive; + this.isExpanded = isExpanded; + this.isShowingContent = isShowingContent; + this.userFullName = userFullName; + this.avatar = avatar; + this.createdAt = createdAt; + this.reblogsCount = reblogsCount; + this.favouritesCount = favouritesCount; + this.inReplyToId = inReplyToId; + this.inReplyToAccountAcct = inReplyToAccountAcct; + this.mentions = mentions; + this.senderId = senderId; + this.rebloggingEnabled = rebloggingEnabled; + this.application = application; + this.statusEmojis = statusEmojis; + this.accountEmojis = accountEmojis; + this.rebloggedByAccountEmojis = rebloggedByAccountEmojis; + this.card = card; + this.isCollapsible = isCollapsible; + this.isCollapsed = isCollapsed; + this.poll = poll; + this.isBot = isBot; + this.isMuted = isMuted; + this.isThreadMuted = isThreadMuted; + this.isUserMuted = isUserMuted; + this.conversationId = conversationId; + this.emojiReactions = emojiReactions; + this.parentVisible = parentVisible; + } + + public String getId() { + return id; + } + + public Spanned getContent() { + return content; + } + + public boolean isReblogged() { + return reblogged; + } + + public boolean isFavourited() { + return favourited; + } + + public boolean isBookmarked() { + return bookmarked; + } + + @Nullable + public String getSpoilerText() { + return spoilerText; + } + + public Status.Visibility getVisibility() { + return visibility; + } + + public List getAttachments() { + return attachments; + } + + @Nullable + public String getRebloggedByUsername() { + return rebloggedByUsername; + } + + public boolean isSensitive() { + return isSensitive; + } + + public boolean isExpanded() { + return isExpanded; + } + + public boolean isShowingContent() { + return isShowingContent; + } + + public boolean isBot(){ return isBot; } + + @Nullable + public String getRebloggedAvatar() { + return rebloggedAvatar; + } + + public String getUserFullName() { + return userFullName; + } + + public String getNickname() { + return nickname; + } + + public String getAvatar() { + return avatar; + } + + public Date getCreatedAt() { + return createdAt; + } + + public int getReblogsCount() { + return reblogsCount; + } + + public int getFavouritesCount() { + return favouritesCount; + } + + @Nullable + public String getInReplyToId() { + return inReplyToId; + } + + public String getInReplyToAccountAcct() { + if(inReplyToAccountAcct != null) { + return inReplyToAccountAcct; + } + return ""; + } + + public String getSenderId() { + return senderId; + } + + public Boolean getRebloggingEnabled() { + return rebloggingEnabled; + } + + @Nullable + public Status.Mention[] getMentions() { + return mentions; + } + + public Status.Application getApplication() { + return application; + } + + public List getStatusEmojis() { + return statusEmojis; + } + + public List getAccountEmojis() { + return accountEmojis; + } + + public boolean getParentVisible() { + return parentVisible; + } + + public List getRebloggedByAccountEmojis() { + return rebloggedByAccountEmojis; + } + + @Nullable + public Card getCard() { + return card; + } + + /** + * Specifies whether the content of this post is allowed to be collapsed or if it should show + * all content regardless. + * + * @return Whether the post is collapsible or never collapsed. + */ + public boolean isCollapsible() { + return isCollapsible; + } + + /** + * Specifies whether the content of this post is currently limited in visibility to the first + * 500 characters or not. + * + * @return Whether the post is collapsed or fully expanded. + */ + public boolean isCollapsed() { + return isCollapsed; + } + + @Nullable + public PollViewData getPoll() { + return poll; + } + + @Override public long getViewDataId() { + // Chance of collision is super low and impact of mistake is low as well + return id.hashCode(); + } + + public boolean isThreadMuted() { + return isThreadMuted; + } + + public boolean isMuted() { + return isMuted; + } + + public boolean isUserMuted() { + return isUserMuted; + } + + @Nullable + public List getEmojiReactions() { + return emojiReactions; + } + + public boolean deepEquals(StatusViewData o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Concrete concrete = (Concrete) o; + return reblogged == concrete.reblogged && + favourited == concrete.favourited && + bookmarked == concrete.bookmarked && + isSensitive == concrete.isSensitive && + isExpanded == concrete.isExpanded && + isShowingContent == concrete.isShowingContent && + isBot == concrete.isBot && + reblogsCount == concrete.reblogsCount && + favouritesCount == concrete.favouritesCount && + rebloggingEnabled == concrete.rebloggingEnabled && + Objects.equals(id, concrete.id) && + Objects.equals(content, concrete.content) && + Objects.equals(spoilerText, concrete.spoilerText) && + visibility == concrete.visibility && + Objects.equals(attachments, concrete.attachments) && + Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) && + Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) && + Objects.equals(userFullName, concrete.userFullName) && + Objects.equals(nickname, concrete.nickname) && + Objects.equals(avatar, concrete.avatar) && + Objects.equals(createdAt, concrete.createdAt) && + Objects.equals(inReplyToId, concrete.inReplyToId) && + Objects.equals(inReplyToAccountAcct, concrete.inReplyToAccountAcct) && + Arrays.equals(mentions, concrete.mentions) && + Objects.equals(senderId, concrete.senderId) && + Objects.equals(application, concrete.application) && + Objects.equals(statusEmojis, concrete.statusEmojis) && + Objects.equals(accountEmojis, concrete.accountEmojis) && + Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) && + Objects.equals(card, concrete.card) && + Objects.equals(poll, concrete.poll) && + isCollapsed == concrete.isCollapsed && + isMuted == concrete.isMuted && + isThreadMuted == concrete.isThreadMuted && + isUserMuted == concrete.isUserMuted && + conversationId == concrete.conversationId && + Objects.equals(emojiReactions, concrete.emojiReactions) && + parentVisible == concrete.parentVisible; + } + + static Spanned replaceCrashingCharacters(Spanned content) { + return (Spanned) replaceCrashingCharacters((CharSequence) content); + } + + static CharSequence replaceCrashingCharacters(CharSequence content) { + boolean replacing = false; + SpannableStringBuilder builder = null; + int length = content.length(); + + for (int index = 0; index < length; ++index) { + char character = content.charAt(index); + + // If there are more than one or two, switch to a map + if (character == SOFT_HYPHEN) { + if (!replacing) { + replacing = true; + builder = new SpannableStringBuilder(content, 0, index); + } + builder.append(ASCII_HYPHEN); + } else if (replacing) { + builder.append(character); + } + } + + return replacing ? builder : content; + } + } + + public static final class Placeholder extends StatusViewData { + private final boolean isLoading; + private final String id; + + public Placeholder(String id, boolean isLoading) { + this.id = id; + this.isLoading = isLoading; + } + + public boolean isLoading() { + return isLoading; + } + + public String getId() { + return id; + } + + @Override public long getViewDataId() { + return id.hashCode(); + } + + @Override public boolean deepEquals(StatusViewData other) { + if (!(other instanceof Placeholder)) return false; + Placeholder that = (Placeholder) other; + return isLoading == that.isLoading && id.equals(that.id); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Placeholder that = (Placeholder) o; + + return deepEquals(that); + } + + @Override + public int hashCode() { + int result = (isLoading ? 1 : 0); + result = 31 * result + id.hashCode(); + return result; + } + } + + public static class Builder { + private String id; + private Spanned content; + private boolean reblogged; + private boolean favourited; + private boolean bookmarked; + private String spoilerText; + private Status.Visibility visibility; + private List attachments; + private String rebloggedByUsername; + private String rebloggedAvatar; + private boolean isSensitive; + private boolean isExpanded; + private boolean isShowingContent; + private String userFullName; + private String nickname; + private String avatar; + private Date createdAt; + private int reblogsCount; + private int favouritesCount; + private String inReplyToId; + private String inReplyToAccountAcct; + private Status.Mention[] mentions; + private String senderId; + private boolean rebloggingEnabled; + private Status.Application application; + private List statusEmojis; + private List accountEmojis; + private List rebloggedByAccountEmojis; + private Card card; + private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */ + private boolean isCollapsed; /** Whether the status is shown partially or fully */ + private PollViewData poll; + private boolean isBot; + private boolean isMuted; + private boolean isThreadMuted; + private boolean isUserMuted; + private int conversationId; + private List emojiReactions; + private boolean parentVisible; + + public Builder() { + } + + public Builder(final StatusViewData.Concrete viewData) { + id = viewData.id; + content = viewData.content; + reblogged = viewData.reblogged; + favourited = viewData.favourited; + bookmarked = viewData.bookmarked; + spoilerText = viewData.spoilerText; + visibility = viewData.visibility; + attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); + rebloggedByUsername = viewData.rebloggedByUsername; + rebloggedAvatar = viewData.rebloggedAvatar; + isSensitive = viewData.isSensitive; + isExpanded = viewData.isExpanded; + isShowingContent = viewData.isShowingContent; + userFullName = viewData.userFullName; + nickname = viewData.nickname; + avatar = viewData.avatar; + createdAt = new Date(viewData.createdAt.getTime()); + reblogsCount = viewData.reblogsCount; + favouritesCount = viewData.favouritesCount; + inReplyToId = viewData.inReplyToId; + inReplyToAccountAcct = viewData.inReplyToAccountAcct; + mentions = viewData.mentions == null ? null : viewData.mentions.clone(); + senderId = viewData.senderId; + rebloggingEnabled = viewData.rebloggingEnabled; + application = viewData.application; + statusEmojis = viewData.getStatusEmojis(); + accountEmojis = viewData.getAccountEmojis(); + card = viewData.getCard(); + isCollapsible = viewData.isCollapsible(); + isCollapsed = viewData.isCollapsed(); + poll = viewData.poll; + isBot = viewData.isBot(); + isMuted = viewData.isMuted; + isThreadMuted = viewData.isThreadMuted; + isUserMuted = viewData.isUserMuted; + emojiReactions = viewData.emojiReactions; + parentVisible = viewData.parentVisible; + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setContent(Spanned content) { + this.content = content; + return this; + } + + public Builder setReblogged(boolean reblogged) { + this.reblogged = reblogged; + return this; + } + + public Builder setFavourited(boolean favourited) { + this.favourited = favourited; + return this; + } + + public Builder setBookmarked(boolean bookmarked) { + this.bookmarked = bookmarked; + return this; + } + + public Builder setSpoilerText(String spoilerText) { + this.spoilerText = spoilerText; + return this; + } + + public Builder setVisibility(Status.Visibility visibility) { + this.visibility = visibility; + return this; + } + + public Builder setAttachments(List attachments) { + this.attachments = attachments; + return this; + } + + public Builder setRebloggedByUsername(String rebloggedByUsername) { + this.rebloggedByUsername = rebloggedByUsername; + return this; + } + + public Builder setRebloggedAvatar(String rebloggedAvatar) { + this.rebloggedAvatar = rebloggedAvatar; + return this; + } + + public Builder setSensitive(boolean sensitive) { + this.isSensitive = sensitive; + return this; + } + + public Builder setIsExpanded(boolean isExpanded) { + this.isExpanded = isExpanded; + return this; + } + + public Builder setIsShowingSensitiveContent(boolean isShowingSensitiveContent) { + this.isShowingContent = isShowingSensitiveContent; + return this; + } + + public Builder setIsBot(boolean isBot) { + this.isBot = isBot; + return this; + } + + public Builder setUserFullName(String userFullName) { + this.userFullName = userFullName; + return this; + } + + public Builder setNickname(String nickname) { + this.nickname = nickname; + return this; + } + + public Builder setAvatar(String avatar) { + this.avatar = avatar; + return this; + } + + public Builder setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + return this; + } + + public Builder setReblogsCount(int reblogsCount) { + this.reblogsCount = reblogsCount; + return this; + } + + public Builder setFavouritesCount(int favouritesCount) { + this.favouritesCount = favouritesCount; + return this; + } + + public Builder setInReplyToId(String inReplyToId) { + this.inReplyToId = inReplyToId; + return this; + } + + public Builder setInReplyToAccountAcct(String inReplyToAccountAcct) { + this.inReplyToAccountAcct = inReplyToAccountAcct; + return this; + } + + public Builder setMentions(Status.Mention[] mentions) { + this.mentions = mentions; + return this; + } + + public Builder setSenderId(String senderId) { + this.senderId = senderId; + return this; + } + + public Builder setRebloggingEnabled(boolean rebloggingEnabled) { + this.rebloggingEnabled = rebloggingEnabled; + return this; + } + + public Builder setApplication(Status.Application application) { + this.application = application; + return this; + } + + public Builder setStatusEmojis(List emojis) { + this.statusEmojis = emojis; + return this; + } + + public Builder setAccountEmojis(List emojis) { + this.accountEmojis = emojis; + return this; + } + + public Builder setParentVisible(boolean parentVisible) { + this.parentVisible = parentVisible; + return this; + } + + public Builder setRebloggedByEmojis(List emojis) { + this.rebloggedByAccountEmojis = emojis; + return this; + } + + public Builder setCard(Card card) { + this.card = card; + return this; + } + + /** + * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to support collapsing + * its content limiting the visible length when collapsed at 500 characters, + * + * @param collapsible Whether the status should support being collapsed or not. + * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. + */ + public Builder setCollapsible(boolean collapsible) { + isCollapsible = collapsible; + return this; + } + + /** + * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to start in a collapsed + * state, hiding partially the content of the post if it exceeds a certain amount of characters. + * + * @param collapsed Whether to show the full content of the status or not. + * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. + */ + public Builder setCollapsed(boolean collapsed) { + isCollapsed = collapsed; + return this; + } + + public Builder setPoll(Poll poll) { + this.poll = PollViewDataKt.toViewData(poll); + return this; + } + + public Builder setMuted(Boolean isMuted) { + this.isMuted = isMuted; + return this; + } + + public Builder setUserMuted(Boolean isUserMuted) { + this.isUserMuted = isUserMuted; + return this; + } + + public Builder setThreadMuted(Boolean isThreadMuted) { + this.isThreadMuted = isThreadMuted; + return this; + } + + public Builder setConversationId(int conversationId) { + this.conversationId = conversationId; + return this; + } + + public Builder setEmojiReactions(List emojiReactions) { + this.emojiReactions = emojiReactions; + return this; + } + + public StatusViewData.Concrete createStatusViewData() { + if (this.statusEmojis == null) statusEmojis = Collections.emptyList(); + if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); + if (this.createdAt == null) createdAt = new Date(); + + return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, + visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, + isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, + favouritesCount, inReplyToId, inReplyToAccountAcct, mentions, senderId, rebloggingEnabled, application, + statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isMuted, isThreadMuted, + isUserMuted, conversationId, emojiReactions, parentVisible); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt new file mode 100644 index 0000000..934d686 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -0,0 +1,313 @@ +package com.keylesspalace.tusky.viewmodel + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Field +import com.keylesspalace.tusky.entity.IdentityProof +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class AccountViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub, + private val accountManager: AccountManager +) : RxAwareViewModel() { + + val accountData = MutableLiveData>() + val relationshipData = MutableLiveData>() + + val noteSaved = MutableLiveData() + + private val identityProofData = MutableLiveData>() + + val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs -> + identityProofs.orEmpty().map { Either.Left(it) } + .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) + } + + val isRefreshing = MutableLiveData().apply { value = false } + private var isDataLoading = false + + lateinit var accountId: String + var isSelf = false + + private var noteDisposable: Disposable? = null + + init { + eventHub.events + .subscribe { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { + accountData.postValue(Success(event.newProfileData)) + } + }.autoDispose() + } + + private fun obtainAccount(reload: Boolean = false) { + if (accountData.value == null || reload) { + isDataLoading = true + accountData.postValue(Loading()) + + mastodonApi.account(accountId) + .subscribe({ account -> + accountData.postValue(Success(account)) + isDataLoading = false + isRefreshing.postValue(false) + }, {t -> + Log.w(TAG, "failed obtaining account", t) + accountData.postValue(Error()) + isDataLoading = false + isRefreshing.postValue(false) + }) + .autoDispose() + } + } + + private fun obtainRelationship(reload: Boolean = false) { + if (relationshipData.value == null || reload) { + + relationshipData.postValue(Loading()) + + mastodonApi.relationships(listOf(accountId)) + .subscribe({ relationships -> + relationshipData.postValue(Success(relationships[0])) + }, { t -> + Log.w(TAG, "failed obtaining relationships", t) + relationshipData.postValue(Error()) + }) + .autoDispose() + } + } + + private fun obtainIdentityProof(reload: Boolean = false) { + if (identityProofData.value == null || reload) { + + mastodonApi.identityProofs(accountId) + .subscribe({ proofs -> + identityProofData.postValue(proofs) + }, { t -> + Log.w(TAG, "failed obtaining identity proofs", t) + }) + .autoDispose() + } + } + + fun changeFollowState() { + val relationship = relationshipData.value?.data + if (relationship?.following == true || relationship?.requested == true) { + changeRelationship(RelationShipAction.UNFOLLOW) + } else { + changeRelationship(RelationShipAction.FOLLOW) + } + } + + fun changeBlockState() { + if (relationshipData.value?.data?.blocking == true) { + changeRelationship(RelationShipAction.UNBLOCK) + } else { + changeRelationship(RelationShipAction.BLOCK) + } + } + + fun muteAccount(notifications: Boolean, duration: Int) { + changeRelationship(RelationShipAction.MUTE, notifications, duration) + } + + fun unmuteAccount() { + changeRelationship(RelationShipAction.UNMUTE) + } + + fun changeSubscribingState() { + val relationship = relationshipData.value?.data + if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */ + || relationship?.subscribing == true /* Pleroma */ ) { + changeRelationship(RelationShipAction.UNSUBSCRIBE) + } else { + changeRelationship(RelationShipAction.SUBSCRIBE) + } + } + + fun blockDomain(instance: String) { + mastodonApi.blockDomain(instance).enqueue(object: Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + eventHub.dispatch(DomainMuteEvent(instance)) + val relation = relationshipData.value?.data + if(relation != null) { + relationshipData.postValue(Success(relation.copy(blockingDomain = true))) + } + } else { + Log.e(TAG, "Error muting %s".format(instance)) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error muting %s".format(instance), t) + } + }) + } + + fun unblockDomain(instance: String) { + mastodonApi.unblockDomain(instance).enqueue(object: Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val relation = relationshipData.value?.data + if(relation != null) { + relationshipData.postValue(Success(relation.copy(blockingDomain = false))) + } + } else { + Log.e(TAG, "Error unmuting %s".format(instance)) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "Error unmuting %s".format(instance), t) + } + }) + } + + fun changeShowReblogsState() { + if (relationshipData.value?.data?.showingReblogs == true) { + changeRelationship(RelationShipAction.FOLLOW, false) + } else { + changeRelationship(RelationShipAction.FOLLOW, true) + } + } + + /** + * @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE + */ + private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null, duration: Int? = null) { + val relation = relationshipData.value?.data + val account = accountData.value?.data + val isMastodon = relationshipData.value?.data?.notifying != null + + if (relation != null && account != null) { + // optimistically post new state for faster response + + val newRelation = when (relationshipAction) { + RelationShipAction.FOLLOW -> { + if (account.locked) { + relation.copy(requested = true) + } else { + relation.copy(following = true) + } + } + RelationShipAction.UNFOLLOW -> relation.copy(following = false) + RelationShipAction.BLOCK -> relation.copy(blocking = true) + RelationShipAction.UNBLOCK -> relation.copy(blocking = false) + RelationShipAction.MUTE -> relation.copy(muting = true) + RelationShipAction.UNMUTE -> relation.copy(muting = false) + RelationShipAction.SUBSCRIBE -> { + if(isMastodon) + relation.copy(notifying = true) + else relation.copy(subscribing = true) + } + RelationShipAction.UNSUBSCRIBE -> { + if(isMastodon) + relation.copy(notifying = false) + else relation.copy(subscribing = false) + } + } + relationshipData.postValue(Loading(newRelation)) + } + + when (relationshipAction) { + RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true) + RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) + RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) + RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) + RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration) + RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + RelationShipAction.SUBSCRIBE -> { + if(isMastodon) + mastodonApi.followAccount(accountId, notify = true) + else mastodonApi.subscribeAccount(accountId) + } + RelationShipAction.UNSUBSCRIBE -> { + if(isMastodon) + mastodonApi.followAccount(accountId, notify = false) + else mastodonApi.unsubscribeAccount(accountId) + } + }.subscribe( + { relationship -> + relationshipData.postValue(Success(relationship)) + + when (relationshipAction) { + RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) + RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) + RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId, true)) + RelationShipAction.UNMUTE -> eventHub.dispatch(MuteEvent(accountId, false)) + else -> { + } + } + }, + { + relationshipData.postValue(Error(relation)) + } + ) + .autoDispose() + } + + fun noteChanged(newNote: String) { + noteSaved.postValue(false) + noteDisposable?.dispose() + noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS) + .flatMap { + mastodonApi.updateAccountNote(accountId, newNote) + } + .doOnSuccess { + noteSaved.postValue(true) + } + .delay(4, TimeUnit.SECONDS) + .subscribe({ + noteSaved.postValue(false) + }, { + Log.e(TAG, "Error updating note", it) + }) + } + + override fun onCleared() { + super.onCleared() + noteDisposable?.dispose() + } + + fun refresh() { + reload(true) + } + + private fun reload(isReload: Boolean = false) { + if (isDataLoading) + return + accountId.let { + obtainAccount(isReload) + obtainIdentityProof() + if (!isSelf) + obtainRelationship(isReload) + } + } + + fun setAccountInfo(accountId: String) { + this.accountId = accountId + this.isSelf = accountManager.activeAccount?.accountId == accountId + reload(false) + } + + enum class RelationShipAction { + FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE + } + + companion object { + const val TAG = "AccountViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt new file mode 100644 index 0000000..1dc4122 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -0,0 +1,92 @@ +/* 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.viewmodel + +import android.util.Log +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.network.MastodonApi +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.RxAwareViewModel +import com.keylesspalace.tusky.util.withoutFirstWhich +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import javax.inject.Inject + +data class State(val accounts: Either>, val searchResult: List?) + +class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() { + + val state: Observable get() = _state + private val _state = BehaviorSubject.createDefault(State(Right(listOf()), null)) + + fun load(listId: String) { + val state = _state.value!! + if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) { + api.getAccountsInList(listId, 0).subscribe({ accounts -> + updateState { copy(accounts = Right(accounts)) } + }, { e -> + updateState { copy(accounts = Left(e)) } + }).autoDispose() + } + } + + fun addAccountToList(listId: String, account: Account) { + api.addCountToList(listId, listOf(account.id)) + .subscribe({ + updateState { + copy(accounts = accounts.map { it + account }) + } + }, { + Log.i(javaClass.simpleName, + "Failed to add account to the list: ${account.username}") + }) + .autoDispose() + } + + fun deleteAccountFromList(listId: String, accountId: String) { + api.deleteAccountFromList(listId, listOf(accountId)) + .subscribe({ + updateState { + copy(accounts = accounts.map { accounts -> + accounts.withoutFirstWhich { it.id == accountId } + }) + } + }, { + Log.i(javaClass.simpleName, "Failed to remove account from thelist: $accountId") + }) + .autoDispose() + } + + fun search(query: String) { + when { + query.isEmpty() -> updateState { copy(searchResult = null) } + query.isBlank() -> updateState { copy(searchResult = listOf()) } + else -> api.searchAccounts(query, null, 10, true) + .subscribe({ result -> + updateState { copy(searchResult = result) } + }, { + updateState { copy(searchResult = listOf()) } + }).autoDispose() + } + } + + private inline fun updateState(crossinline fn: State.() -> State) { + _state.onNext(fn(_state.value!!)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt new file mode 100644 index 0000000..24a7339 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -0,0 +1,288 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE +import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT +import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.StringField +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.json.JSONException +import org.json.JSONObject +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.OutputStream +import javax.inject.Inject + +private const val HEADER_FILE_NAME = "header.png" +private const val AVATAR_FILE_NAME = "avatar.png" + +private const val TAG = "EditProfileViewModel" + +class EditProfileViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +): ViewModel() { + + val profileData = MutableLiveData>() + val avatarData = MutableLiveData>() + val headerData = MutableLiveData>() + val saveData = MutableLiveData>() + val instanceData = MutableLiveData>() + + private var oldProfileData: Account? = null + + private val disposeables = CompositeDisposable() + + fun obtainProfile() { + if(profileData.value == null || profileData.value is Error) { + + profileData.postValue(Loading()) + + mastodonApi.accountVerifyCredentials() + .subscribe( + {profile -> + oldProfileData = profile + profileData.postValue(Success(profile)) + }, + { + profileData.postValue(Error()) + }) + .addTo(disposeables) + + } + } + + fun newAvatar(uri: Uri, context: Context) { + val cacheFile = getCacheFileForName(context, AVATAR_FILE_NAME) + + resizeImage(uri, context, AVATAR_SIZE, AVATAR_SIZE, cacheFile, avatarData) + } + + fun newHeader(uri: Uri, context: Context) { + val cacheFile = getCacheFileForName(context, HEADER_FILE_NAME) + + resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData) + } + + private fun resizeImage(uri: Uri, + context: Context, + resizeWidth: Int, + resizeHeight: Int, + cacheFile: File, + imageLiveData: MutableLiveData>) { + + Single.fromCallable { + val contentResolver = context.contentResolver + val sourceBitmap = getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight) + + if (sourceBitmap == null) { + throw Exception() + } + + //dont upscale image if its smaller than the desired size + val bitmap = + if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) { + sourceBitmap + } else { + Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true) + } + + if (!saveBitmapToFile(bitmap, cacheFile)) { + throw Exception() + } + + bitmap + }.subscribeOn(Schedulers.io()) + .subscribe({ + imageLiveData.postValue(Success(it)) + }, { + imageLiveData.postValue(Error()) + }) + .addTo(disposeables) + } + + fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List, context: Context) { + + if(saveData.value is Loading || profileData.value !is Success) { + return + } + + val displayName = if (oldProfileData?.displayName == newDisplayName) { + null + } else { + newDisplayName.toRequestBody(MultipartBody.FORM) + } + + val note = if (oldProfileData?.source?.note == newNote) { + null + } else { + newNote.toRequestBody(MultipartBody.FORM) + } + + val locked = if (oldProfileData?.locked == newLocked) { + null + } else { + newLocked.toString().toRequestBody(MultipartBody.FORM) + } + + val avatar = if (avatarData.value is Success && avatarData.value?.data != null) { + val avatarBody = getCacheFileForName(context, AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) + MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody) + } else { + null + } + + val header = if (headerData.value is Success && headerData.value?.data != null) { + val headerBody = getCacheFileForName(context, HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) + MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody) + } else { + null + } + + // when one field changed, all have to be sent or they unchanged ones would get overridden + val fieldsUnchanged = oldProfileData?.source?.fields == newFields + val field1 = calculateFieldToUpdate(newFields.getOrNull(0), fieldsUnchanged) + val field2 = calculateFieldToUpdate(newFields.getOrNull(1), fieldsUnchanged) + val field3 = calculateFieldToUpdate(newFields.getOrNull(2), fieldsUnchanged) + val field4 = calculateFieldToUpdate(newFields.getOrNull(3), fieldsUnchanged) + + if (displayName == null && note == null && locked == null && avatar == null && header == null + && field1 == null && field2 == null && field3 == null && field4 == null) { + /** if nothing has changed, there is no need to make a network request */ + saveData.postValue(Success()) + return + } + + mastodonApi.accountUpdateCredentials(displayName, note, locked, avatar, header, + field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second + ).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val newProfileData = response.body() + if (!response.isSuccessful || newProfileData == null) { + val errorResponse = response.errorBody()?.string() + val errorMsg = if(!errorResponse.isNullOrBlank()) { + try { + JSONObject(errorResponse).optString("error", null) + } catch (e: JSONException) { + null + } + } else { + null + } + saveData.postValue(Error(errorMessage = errorMsg)) + return + } + saveData.postValue(Success()) + eventHub.dispatch(ProfileEditedEvent(newProfileData)) + } + + override fun onFailure(call: Call, t: Throwable) { + saveData.postValue(Error()) + } + }) + + } + + // cache activity state for rotation change + fun updateProfile(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List) { + if(profileData.value is Success) { + val newProfileSource = profileData.value?.data?.source?.copy(note = newNote, fields = newFields) + val newProfile = profileData.value?.data?.copy(displayName = newDisplayName, + locked = newLocked, source = newProfileSource) + + profileData.postValue(Success(newProfile)) + } + + } + + + private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair? { + if(fieldsUnchanged || newField == null) { + return null + } + return Pair( + newField.name.toRequestBody(MultipartBody.FORM), + newField.value.toRequestBody(MultipartBody.FORM) + ) + } + + private fun getCacheFileForName(context: Context, filename: String): File { + return File(context.cacheDir, filename) + } + + private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean { + + val outputStream: OutputStream + + try { + outputStream = FileOutputStream(file) + } catch (e: FileNotFoundException) { + Log.w(TAG, Log.getStackTraceString(e)) + return false + } + + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + IOUtils.closeQuietly(outputStream) + + return true + } + + override fun onCleared() { + disposeables.dispose() + } + + fun obtainInstance() { + if(instanceData.value == null || instanceData.value is Error) { + instanceData.postValue(Loading()) + + mastodonApi.getInstance().subscribe( + { instance -> + instanceData.postValue(Success(instance)) + }, + { + instanceData.postValue(Error()) + }) + .addTo(disposeables) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt new file mode 100644 index 0000000..22f509b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt @@ -0,0 +1,111 @@ +/* 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.viewmodel + +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.replacedFirstWhich +import com.keylesspalace.tusky.util.withoutFirstWhich +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import java.io.IOException +import java.net.ConnectException +import javax.inject.Inject + + +internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() { + enum class LoadingState { + INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER + } + + enum class Event { + CREATE_ERROR, DELETE_ERROR, RENAME_ERROR + } + + data class State(val lists: List, val loadingState: LoadingState) + + val state: Observable get() = _state + val events: Observable get() = _events + private val _state = BehaviorSubject.createDefault(State(listOf(), LoadingState.INITIAL)) + private val _events = PublishSubject.create() + + fun retryLoading() { + loadIfNeeded() + } + + private fun loadIfNeeded() { + val state = _state.value!! + if (state.loadingState == LoadingState.LOADING || state.lists.isNotEmpty()) return + updateState { + copy(loadingState = LoadingState.LOADING) + } + + api.getLists().subscribe({ lists -> + updateState { + copy( + lists = lists, + loadingState = LoadingState.LOADED + ) + } + }, { err -> + updateState { + copy(loadingState = if (err is IOException || err is ConnectException) + LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER) + } + }).autoDispose() + } + + fun createNewList(listName: String) { + api.createList(listName).subscribe({ list -> + updateState { + copy(lists = lists + list) + } + }, { + sendEvent(Event.CREATE_ERROR) + }).autoDispose() + } + + fun renameList(listId: String, listName: String) { + api.updateList(listId, listName).subscribe({ list -> + updateState { + copy(lists = lists.replacedFirstWhich(list) { it.id == listId }) + } + }, { + sendEvent(Event.RENAME_ERROR) + }).autoDispose() + } + + fun deleteList(listId: String) { + api.deleteList(listId).subscribe({ + updateState { + copy(lists = lists.withoutFirstWhich { it.id == listId }) + } + }, { + sendEvent(Event.DELETE_ERROR) + }).autoDispose() + } + + private inline fun updateState(crossinline fn: State.() -> State) { + _state.onNext(fn(_state.value!!)) + } + + private fun sendEvent(event: Event) { + _events.onNext(event) + } +} diff --git a/app/src/main/res/anim/explode.xml b/app/src/main/res/anim/explode.xml new file mode 100644 index 0000000..08001ae --- /dev/null +++ b/app/src/main/res/anim/explode.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..972e757 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..9b48ae8 --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_from_left.xml b/app/src/main/res/anim/slide_from_left.xml new file mode 100644 index 0000000..5c7fe52 --- /dev/null +++ b/app/src/main/res/anim/slide_from_left.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_from_right.xml b/app/src/main/res/anim/slide_from_right.xml new file mode 100644 index 0000000..3c595d0 --- /dev/null +++ b/app/src/main/res/anim/slide_from_right.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_to_left.xml b/app/src/main/res/anim/slide_to_left.xml new file mode 100644 index 0000000..21688e2 --- /dev/null +++ b/app/src/main/res/anim/slide_to_left.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_to_right.xml b/app/src/main/res/anim/slide_to_right.xml new file mode 100644 index 0000000..8ded764 --- /dev/null +++ b/app/src/main/res/anim/slide_to_right.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/account_tab_font_color.xml b/app/src/main/res/color/account_tab_font_color.xml new file mode 100644 index 0000000..c81c01a --- /dev/null +++ b/app/src/main/res/color/account_tab_font_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_background_transparent_60.xml b/app/src/main/res/color/color_background_transparent_60.xml new file mode 100644 index 0000000..0a09f2a --- /dev/null +++ b/app/src/main/res/color/color_background_transparent_60.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/compound_button_color.xml b/app/src/main/res/color/compound_button_color.xml new file mode 100644 index 0000000..8b151c7 --- /dev/null +++ b/app/src/main/res/color/compound_button_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/emoji_reaction_button.xml b/app/src/main/res/color/emoji_reaction_button.xml new file mode 100644 index 0000000..7d1c700 --- /dev/null +++ b/app/src/main/res/color/emoji_reaction_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/text_input_layout_box_stroke_color.xml b/app/src/main/res/color/text_input_layout_box_stroke_color.xml new file mode 100644 index 0000000..5b21f78 --- /dev/null +++ b/app/src/main/res/color/text_input_layout_box_stroke_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/elephant_error.png b/app/src/main/res/drawable-hdpi/elephant_error.png new file mode 100644 index 0000000000000000000000000000000000000000..71310c5e890f723d03fbf7860fd7e0c68602a54c GIT binary patch literal 31252 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 O0000qDG00009a7bBm001r{ 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

l2p9LFxB5aHn|7KOh(0S_LOx8_bM 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/splash.png b/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e1faaa3ae761541886d25f19c39b2d7d02c332ff GIT binary patch literal 10238 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 Nla*AGs1Y*?_#cnPN!b7Z literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/elephant_error.png b/app/src/main/res/drawable-mdpi/elephant_error.png new file mode 100644 index 0000000000000000000000000000000000000000..a81667a93fa24dba52bf6535bfbc54c676338436 GIT binary patch literal 17853 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/elephant_friend.png b/app/src/main/res/drawable-mdpi/elephant_friend.png new file mode 100644 index 0000000000000000000000000000000000000000..798a4f545440fd54559fb8c883abaee65bf68bb4 GIT binary patch literal 19435 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 R0OaGL2taTt+ckL@$e9SdL&?Tt+mE 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v26/ic_launcher_foreground.xml b/app/src/main/res/drawable-v26/ic_launcher_foreground.xml new file mode 100644 index 0000000..677bf6b --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/drawable-v26/launcher_shadow_gradient.xml b/app/src/main/res/drawable-v26/launcher_shadow_gradient.xml new file mode 100644 index 0000000..98ce833 --- /dev/null +++ b/app/src/main/res/drawable-v26/launcher_shadow_gradient.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/elephant_error.png b/app/src/main/res/drawable-xhdpi/elephant_error.png new file mode 100644 index 0000000000000000000000000000000000000000..e21b51e8db872facc8ec6d1452c3f44abc3f42a8 GIT binary patch literal 47021 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

)Cya!V?0J?@AJo5`LZzL|W$V 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

`vlh63!SAw@+xB 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

6aw-tCH{7zTS3$yd{^ON~f`gZvqKZ*CIyp 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 FV1mIh^i%)< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/elephant_offline.png b/app/src/main/res/drawable-xhdpi/elephant_offline.png new file mode 100644 index 0000000000000000000000000000000000000000..39775523a25d45a9ce3c00c81d01528a72f7c9c7 GIT binary patch literal 45673 zcmV)CK*GO?P)pH8^fnJ}NLba!@gIR5*-fUUXDAk6~LiH8yimGmc?f zj$&MnVqA@7UR_8uj$>VoV_jTFGLKmNDN?qlv zbLXvgMpI`nF)>6@WbU?t>92WoPcCjrDDu0B-HG&SeHtMuB;epfd3+|ZC>Tg{qjgnCZi?oqAlSf?w^-xt(!n*`#vM zrIO&Mbg6$evT#*QMKHZ~TgIM^qJCbFUp{3>Fw>uJk#SV5hGM>ziRjM1hF%<_cW#Aa zMB>S~Ku1wJKS!8rW8K2CZ9+SHRv3%9?LTV0oFdkA>^VsC-yfzKK|2Tv}OhjBQ+5b!k~^kiYQK zVoydx;;MeA;!Csess(a=n{>q;*T)o_Mu_Q`o_j)w-eG!C|a>P=u(~ z|NH5jhH!;E*-Lg59v2 z?(fLp_9`3z03ZNKL_t(|+U%UaYa(kG$2WJ4KR`hcjSw#DeS;v}MhBazv=~QcW!6Sm z(NRey1Z-k8;0$vM^B*kfG<%zrvAac@wM=I-rQ7*h_)om&JkK*f%#V%kO?)?rCrv^^ z$ehpfJ-?nadH3$cnQ!0Y4M#%3c2y5QG!;}}e-mVJ6Z`!?I}A_a|GiK=7R7Mm_+Hh9)A_=j z=wJplU7t+M#e6zcZ4hGS`a1Uehduw!iT}nt2l1%u@Pqf!`IElUNO52(E=5dEkuSNF zZ0M84e5|P=qmY596bkRe@bdzQiLSt-jlui6A%nY94;zq>4d4KJu(Crp7ChKd_z3}o z<54HJYtyCK(ed!7)-j$-8M+A=M2o`Vj>5(QIJA$yI$Tcl28}lx7-+@l00x>IAban$ zuz>&~wg9@r>0)AVTH~~QY!c7G%vroXyl2+Yyj%=LfQfVil9IVG@DgwO-1NW5rP;Edfw6~A`)wzwK6S2 z;Hs13w?Y6B0wmBQMi`VT z`4cRT#BRv*HUPv&Mfji+74N8UeC&1d*s*aZ~^phzux#d`>3?YV?(61m9lh5WF7< zj8w6R1TxcEubJDP|A}E$DwI|N5tV^Zt^tDFD;C23UhpUY?hlN#<&Y2%KUD_XCBXA! zN}8`R8Q27>G#Ma*T+WzxTdlKKml6C2@oxc)KqGB28508PakIUB0%S~DutO0mOPdY? zNs@ST*cuhjUX>6CfGlc2TNpakfY$G|tNAUa0Y*TgV>%ZD+MHDc8-XN&5Y9%U*CPb~ zhZqBJxiENrNHX#8%OKq=a2&T)0>r;;Qd%PYaj#lsBB1G*Ab_7dy#xlM*C7P|LF`*L zn?(So!EuxTbOY`5pbU??9SANB5C(W#06j7t6EzuuS^-l5CBSpWT*Tt-4 zaI-MDltn=OSW2hcC3@x_XCQC7%S)|c$hQzkj^)_3Ss6J9LKy%M0#+Qt$R+9X3>FYC zSP1^#06>5M_^1;%QvzCDNvG3I>(s3!gUunx`b1wvKz%Rem=G)oHSj+CDS0;mp7szj z2mx|f;lf}YV*;^q1B<5!d0j^w)Ww7z4kiMKR zVJRSZL<9f;OnWQKm=}vJB+!OL(6o|Z z;Ynp+%^=V?b38U(P^>8AI_z|vC7*Lepj84yyVDHT6E8>zAOMzwv!kmMXDO($Y$1U@ zAy6f^wu;4oqv!PBqF^mB^=m$F%!lI%=jei{AV8gYgfYAWc z|GdU0OZ=)6i+#8M1FI2;7DG^uwz~5Q5=iV~+Y0mqY0wN%3VT+8vBP92ytS+K_~ zA-XA7DOJ8)b=3vOJ9AQt!ioqU?#T%OnZDb9az%QaUokEF_d-eA|0n?c?sGvgR20Xv zLAy=EkUb`DDu4i&N}60;PbP3mp_maRb`MF2GD ze^)U2ABhl-eyy!LpC=qlxcq;Eo9TBRi!((`1braT#%jnWKoOKHN_8+G0!gT3;pzJgpZm0Rics4|K5H4Ap=kE ze>;-XHB1D@RYt(MEE!ZW5DBF!1CXYfqEz74M_@NqgaB@+&{a8VO66D;?=axj8x7Jt zKs9io>wU1#H@`|W^7$e9Cmi=@N9xqnADH9Fq$it~+Gst+wMEsV2%0VeR0!)Zuq%Bw zTOi#)p(fY_=tY1~?*fCBbg%&dmj(bpr_(w8headL|JD6_5qI>-21Ym2nj3cBR0IYS0SQALLg4h| zJBC*QLN&QH?A@1)=Bt53(LoqqoZ2xJ^gbe9Stph$Va zwbYE=u^=?t>AMUn(*1`O+h}W|VG+m|&hs^*fg0|ulV9I-5C{ce&|Pde?j|CTEfIhQ zI{nUsA~>%9a}7QSd<*V9U0(Ku(6dKJiDWPo2_p-?f1j8ioF4(_AAMVKK184Eo2yUt z6Zo4t9g5)IK*V8FmKlC&x1k>DW6MlvJvEo z@6Tt@;gR8UncPRZRUnk}$Y8nAYn(I}-%12X01+J5KYzW(>+qu|%*+0yJGcOgL?THn zk-+UAoY%U(Kl*lk`Sq{Q_2c6cV8B$+Fz!QOjEhz^)E)>Bkn?5sehWC~`>ZUK%l9G} zGz&R0*mRGwr2-2x3T^`Pl9d1owZfN~zy=fKM{ELY6u1BebXKuG15klo4LJ->2!i_G zAFr;_El-%2J&Ek2hD#9O{hQ0HuYY~|{CAxqAR>531>7P7)zEmx!VqyI>zyjXZ1&$7 zG-mf8Afd>)%3>J!8gvj=3s>X*$J*J1w3V)LoM~&TpdfT}mx2_eA+0l&^N1bh0-jo`RMzu=u zhEr;kO2?(+<02lvKcCNUZ2?QC(|DM7D*1Fxr=_3_3AR9 zFZ1E^td}_x31~D^zgnwQ*3{u`0FpA85mcvv^w|}hv%1|UzNjilXO_7NcvFBr^n@cI z0&qYHH`ONGd7yT@%>vNF&wPkfU=%^bUIgbR8`AEQ7pHmwLE>Z2P}T;)f)^R^T`S+H z*Ydgr&5GC=9D58523f1%bH~>dNs<&AY>UhOwS^M1J8EGz3|9} z!8j8^keSiAwTt!+7#Kj{|9$y*92ij7oVkMVg9O$piX6)UgaF%zNc2S|3BsRCCgmLb zpVPbaFef_CLXE*PGK?z05!Fv2z^syX;_-h0-5m+}0 zQ3MGRLDpya9WG9}2*FZ)AY_Liq&hkw5Exd-tuvt7HF6rB$OdtKovz3V#Gg(@&gF6n zZFV^V7an~7CzDA4q)70mtklKHfX^nDm*e_;NJYR^pz8o2l-MQz{cH9Kw;=x2ss=$z z!8(Aj3c%15h!AiQ#IqtB>iKa6M9|y2@9|tZZIZ#GY{;w00Gvqhvh0vLCgNS(K)M{B zSQr{{0s#qZNI?J?sfl38)0lwX33OT;r>k57g5Ds@}UF#p%6=T--0aypHEvs7miei zdQ8s^;CwrBx(#lF4upmy?H{bKA4pgLLDbQDhzCZWog}6MDW_yaDxiWWzFcoIB1*@) z6Ioi>qG}?bg>U+la$-=WnGjDfq=s6f1Q;OpEdERcwk0Xu6P%(5g4X6I!jdth0zeS` zI3RA2A1zGy*aAA+K^MFVesQ|RH|<8Gx5<9+eJ<~B^t#||?9)Gh1Y98mNI;gN&yWFl z5hx~i;SiKWMitB$2pAFgo2^Jq&5S~K`~B~}{zn@jgEnHYwYtTIAlf}yqx>}dgiAmZ zfz^ltm*7K*xHuPcAEH!%kH4maVi{y7e5}u66^|epOpZHy9Om5=_~qBA>W4VX2fg$k z!rBXij$HyEKmgLgM(=vc)8~;v2OtEwSq5rv#;@%SsX)&KdBz~`7oKCyMet7YPRlpx zAiwqbm!qT4keq8x7iPeLXM&xK=;l6+S2g=VVJ_|o%#jG7D`1~edFh_WiV=(sXlG=^ z9TFhG0bJ8^Rz~5uR^IQ`&o)CKvA$T@aJhP28wV+50OD9vnzDgf5T&^Yo*26VEX}pv z%$f{LTHuju>weJQ(RsJE<(~OEzV*)C+nslXIT^=)ML=-$^M?Si6eQyrD2K)c3yP5y7xy9h2=8br6BHIj+3`VE?Qo~J_QzQ-E@PW_(j9e~ zS39WQSTEb;owA0-qCGJpvJ&{0ao#yxH5^dvo&6->4cfES-r6WKN^Q(TfoD#ezX(VA1G9 z4 zSd|)!Cdy~pO1`->0~eYh{iVi1Ui3`%62usGj`;?0}i&2olfWV6Ia?-rY z?lxZ;Orldco!Y{ z3|>Ef{@YLgU&Vc2(k36uKm4yJ8cn6$-jKy*CM+&yy0*a!WWH2n zx~sbY1T>1?=LlAQqzb1_BO@@P10t{(2}GtB0fNG-)YvnONP&NDysCnCJ%B7gyOwI9K0*Q6PIaG&h_D53W+=@%0P8kZQC2rSjLlzhqmguM?J>*jxC`$6 zJ&oJk`#H|ghn6RyckQul3YZU-e|q@C5fnw+X{X!lMdHoUks;cmzyUZQV$aN4MzpfT zhC5h2rm5_di?ZEaf|hj;t>u9w$Q|A9g&hTIOWp&3XAabOQ> zwMBbDX>2Ycrx*NjNq>ZzQmutNGH4*yU^JMj$;%-QB=MYDG6;?4IZ&}kxWQorL&OH5 zV~208wf8=WZJOKGw0HLhBB7P@*=v2*uf5MI<^}Z!3#WQe1%P`0AnUr}kh?lVs9kSJf-@K2x$MAlJ^*Ythz$umjTBgqEw zP-PN1OLnp{Zkc}47S&^ywXIC|{gav5KwGjbERV($&qN=e1*lPG zCbac&_J>^q2gV>qfI@n{fS>!k5hQA)B@2%{zQ|&5qmG@Y|9N1n+8YkXmK$uggI_Bf z!3jSU^_q>`48Z?ddhn>hZ@p(N zY=q)M9Vk`;ZJ?m?vyY0L!7Qz=z3B6~?2DW4(Sao?c>6Shv=g@V{ofu~s`?=eNhgi4 zzziB}!#MlgXz96$hj8!`V@uU*7A@l5no4I%_`XJOv`iuCJ&wSwkQg9>p+7o=+hQMf z%Qop?I+xqq+uK=5y`YSWV-VA`M4>MVG38bwA*v4|LBhSxZ6FzCR>2AwA0`ew$NiE6 zQ_LA{6+FOT(7yEFjU;oGW1kv}qIk%e`=w$y#Qo4!@3M4k^=FjX zq5dI;z+{4tvck1Hy*u0>b?1n{KE!>GibbzKzdgCJB_tX-pgr8cO&ARiq*AGH_+=s# zkFwdrc!KwLCr>CuIRV~P`}Yt3`r=j^Hv~X1I5;vGdHWh2Qk^q1P=Uhu97w^@gWigd zNYsbW51|+vKQM#ZtTZ}!nIcU^Rkh`0%it>HN1-jeGxud zSl}YAO)Gxd}?qH0&-bu^2OgSh}%w`bcVU{w}P*;{o||K@_sMxU^K<{HRa~ ziZHnii-YJ(v5bvIOhn#R-sI=Kmy2Hq01e?MG=${qC1wZk$G#M7$A5G_`T+$nXqz*4 zr{eixQKu$9^wKDnIgGQ0V>M)u#_f$B9$k;h91sSj_@&Cef#DId!Bkq-=)A%K1ZfK# zK^Dva7`R-K2trr~a{vzQdij|yI!P4@g>8KiiB#%#svH9`2*tSwpK9dpEQUtljUs~r z0*G^g5`}qu9jIR5LMe#%HXHuR4?$+EBk)G02I-)NFH~$CQ`zpOHjTy~IotnOvZl!i z(gqD%`m$=pn=o=2V31=BJj@L2E_;MQ01Ct*Sa|&&i=S9@*m{`{=7YgPut00OAk2bl zd6TTBjk3*p`sYr0D(9c)FOWCdw2yEI5FxpiiiL0p^eBFm9_LaZVRfQ%Pj;d-6lkF{ZKxrGH0^F|7lSJz&!e#2uQ3DQ=Aq5N=O83XL#TVLITwc@Vef<)*kK6> zi~?{d7T*>(H~qn2aC2u9e((7GfFfVuMuAFAU4?Mlvydpw)#Nb98Iom)jR*o^u-n03 zX7}Ii@J@to88}sSRyyw+PFI`*@maC8>)n))2!wl2E9sil5pwL3-YN^9iH}eT>1IESft_m~ur zA7V*?d19iIJkj$Pk|fQI)#{S8V>j+_r8KNUJ$q~($Mo;80x8wCQMfw{UOOYkO?uxR$E}tvX#Td9agvBM`u*e~-et?0Q0_Y>FBh_7~r2%{l!RzBc z8zNu?nx^Ie!9kCqT1bj5%cz$g{3F+6SEo>9*nhjVxgt}OSvg3Sov&v5hc3^J24p=unnL|Jfi>r$z zYY-52z!vhUcDDb)0sZ65H@ddv41=)N9-N@KtqVL6`5}vC42N@WSN21El=SjwNsK23 z>mN^)DRNC*Q6m_E!e)l%X0agDA8^ji2Hb8+1>{)ZDqoiab6^h+N)}{g+(3%ln~T~S z+C)?Y49FRE*+poiQxGu77M4~4!n@}?h44mFjMAMy^DVE<;SlOJwu|(Sx$E%&03ZNK zL_t(TzCi>z%XDknDM`ckEmfp|{16jkW2ftJ{Vh8)19Nb+My|*-6T|37K?2wt80w#$ z8-6v*CWbV(=<8!gtv>*ciSh#n#HDy9_xjB{3r?Ffz;k4jP|F9(L5ftbwFWOo^Yp{Kj5+kt6=Lq4W@`L*jUckU0{oOvD=ktU-cM7k(f z?&1{6Ib_$1!P4WucM#iADy6f85qF`5CAjqF*7h1&=oD8mVrr&(ZLN_5!?MLn1OSc5I~f~5cL0H?dY)H zyL<20?Z)s)b*ucYk!#JpEg4(GGf^xe2p%v8kB=;Z01$@7bu8bB<_+FwbN2gRDEs4i zemU=aG`uivX*Fa3!GWQw6iA{%XZE2H00a9(sZIe&a{6?>$f}ZacgN=RaYix&EZR|R zltw7}@A8?gW1ObLhNjxVkEZuD_wGe)l%WX-S++cHP@vHtEiSz+E1uTj?mzo?-RE1n zqB&mr4gnvcE+rHte4CH5MZ51 zkr{%A{Qo$1C|I!*2n_V_K3W`ISzm^vd6ut_3=>DYmV zJ5_OOqBV^Z0uKfxkb^yfu$tZH%bVX0mrd5f)BmdD1K%<{x-t(0KGB0@vlA4OzM$vb zX|LDUh;T)P|5JBctTXrCp&nyDUqJ>qRGQSc-(?B98G$ngjzIPJU5D zg%d|(quAJvX3-Eqsajh-qK(_CwXn@EQ|ZNuob*5`tSmvAOT9QWc;P>AF86ufcV;q4 zCW(4AI~OfjZ0mgTJm2Tn`@B5k^@h%d`bVw|K|qj@54S^hkinViG-d!~%uX2b(oPb$ zl}?OBe=2UtaGE@h?-}O4&`O3_069YSic9|IruG0<<Ov{{jZx0v&l4n+oZzX$=4d1|NJu|2q_~Fq#%YJ zOB#`Ck!g)jE)NdIke8L2yR z!h89gyh}z0De_OwIJWtBDt($7S^xn*Sz~+!rQm~9@y9$G`U)f4gLydg+Z$~PB|)bcH~**{fyOUN z@<$#RaGAlusiA($RYD>hki#h=$rZJ??cromv44YWDWViX8VRIJj!K{_0yDUK%6k<) zP?kKWUQhJREr?0a72n#3wDVwzHVS$w5NL#&Y3ssaNMtbDfBABSc;*rrthLdqNUG$( zcb~WgVTb&RT3j)=>cmO!_nbnOqn%W-Cu#%)p|e_2z|tYQaxtclO$v)%iwp*@zyQM1 zG4V;G+izbN$4qI%a!efi#b2?@UrvB3vfJSUIjD#(z$=}r*Aq3~W?AK3ltN;b5n2Tm zVoVWe#IR`L8AllOqBFWQ0|@pd22nD?C3Ntkq6J8+KH|9>nmBtf})+yD^zwxGq`)g zvt`*n{LyMswVtSfV_D^xlqP0kR-jv)sDfZZtW>Nl5e6wpss=|d8yIQx3Ho906VK2H zUMInIQfEXbD6nIeRX(#Qnt^YCD|1B{vO>73((LT_T(y{#BxddF9K*Z@Bd{-Psy&a& z8LUGNb$LO^CuE~sqAf%dnGF1;I|*tHkig}u?P#Q>BD2Hib9~Q~#(0R}pQ`mlH@GS> zDeah{8gpOmHT;k)G!+=Iqyv9>K_GMMqC27+a1yaKGE14tX7gvYc4W!}Q=jiNSz371 zYVR4HIq%H|wI;_^5YXWXqQ-VzX@#yLG%aU9j>Xh)Mi!+5Kd!0Fj);I5L3D>KX3202 zm&c1I>fUckU6Ie{dl8D2WetYz2Ws_1%`C$d>ZtL8PG_H=sDYq|Fwo7d%q>s`{2Eph zrm8t3OTM5~7z|IXXVB$DQH7(4oz%KC$`^BWbaWW%BLGDiaj&3l96=oKeXEIivdj? z1`7wndi??B2O+H4g;12y5R{ye_DruIRvN<_86`_50DT+ztz9yM+=K7ZZxDk+-O12-?Y* zel~ut6yLl0N2dmR$%A7N>&@hG&*HPR2y$cW1| zL>a_bhJWGlc&x^H7it#G;3Rogg2rfl5mKWdH{AjRipofLRO4oZ13*w+3TzYa_Q3>Z zza07J#n`lU;}wTz)A-aHIZUqS^e;NouOd*-vB{o*&o~vho{?kQ8NfS22ulZ zGccH}IW-D$(=Db`sdH?RA=qDBocC7)pxNGf3@Q*m*q-hQERIWuVwXac0Wk!WgbE0* z{4ji9EMN2PEOKjt5SN}h91e5GMa-a^n1T8HT&6=1mbxFRQIMN%d21nnw4k%|GKtmcY88IrM{?5a?Z8>=922|s$Qb93|K{QQh!p&H_#HvnLE_&6{v&N&tt zfFR(A5>;jDj>=OtOCI@wH>xq~I7%ZkFsgz%7z~=77cc{Z(?c$Hc_>~_hqq;R8M;xB zRo69WyzT8&nxAk9hs*#${OkO3phv+4`G48}0zT;9`)_SFDNU9bw9ajO2oVUPAy5dV z^|NpR4I!4VdW#$lj@#CWAUnXro9f9V6-Qu`iWU%rYoe{$lL!gTnv6bK|9bid;A;eQvy^&y!D98^y z5%BQNx;MS!2i}8g!pz~bFu5|85HNOFtpzW0U3CaLJ7eKHwGqtB_$!J+6u>Y5m@X@u z&Pxnh7e1mhVi;C31hUVg#1yoaJdfU}FnLE?U66?kR=ngL zZz0hYuY^s$Pp-n!BM#> zSnh<+MM>2y&L12sXC@fr%0g zI68ntApYt2ZI4Z{g|l2_0EX}i!&C4^BuAjp5&9$f0IGIUgrHQj7!^GcbDx^Qg#_6g zL^Cjj2XLGV-H@@v*=_dxsJaZfCW3bAh&BTL30W9QqM-Qm&jFjQS}St~7>JQk>&ok0 zPJn=h(AFmUqoLv`wpJTHr|zgy6TlOZ-VqOieB!`(-*^Erz=G*rpTq5(v>3YX5dwg~ z>N(|^+l{J1@a@gc&Vp7M0lqI@uh(z+{el`#YqmEa5z~L!e>X90tJObPv`dDdo7-68 zv0wq28hRsXB{HglL;)b=W0>Qm^5sjbM#Pwi2(qtqCvY_}Fm~gz=7`6-8pfS?20{Q3 zcvM#?A_(p&AQ%}WpV*il9FhMKAh1o_KmyPJ9&c>hrt8$M{qqt79S%HFZv?t5LZFpU zDhnb7OSUjfO`(JW^+fIHiIS%gGy}s0#DHv3jX2z<)m2<4PcK)tx*aEKi%S84_q-~C zc0>SH@CaUc;0PpGKm7Fg$Y09LTpWD z3L*v)Cb=wvEyVS4Uhqi5vf4}t1v!FdV63mlMkks<1UIO5bQlqV>3__f-%A^78pqj- zv)zkbC>-{%|AFFq(FAglmXKV`=w#=FNpYaV$xOpxp@j_^-8tZ|V^2Y+3`81xv?*eg zQzNOM!GaZUg0KoA94|yXm-c2orx*Pv_IaN7oyklRHSH00-YTsYLh^p*dA`s4Jn#EH z41p1_eCZ!T{h_N6EY@PNh2zcPf$2}1e&&oQGHO65xDar$w%F^VKp4mzh{MyzzkEDd zSvkS08r`!dD99=%L51+O8EZfQW{|~OY-LRZ1Om39m2}noDOhH38xZ``9s#=rrxvrT zvom9#JpDW}B133M0zlYe!b}yNW(sbkL>N-tsi#l=^Z3bA=fdE(T#&&Gt`lVJR4xk; zKbfj(D6$62GHL`YVbS5_;a>w}2Dcyf_q9RL?I2hJx4bbrpX>U}F~$_ZYYBV1jww)0 z$EOQ^uLK;x9#Zb=Y!NRkd4~wh>jXK9`YT9Sy21O(WsxC}B;6o02vFI85QHy1XV%*b z@yTSQU8c0dF@ZTFKZMfHKfa#grhvMzy|i?%8H2;&I|hOkFK}rP`a5PGEa{r1K{(Eq z0pN*%fdsAy5l9Sy5vYxIw;w)hiy-VESc%z(h4%G=Pc#HCgp0jp*CB;4oxQ!d)O#t1 zAHHK1Wb88wf{X~%#C2kjKm-s!Sr!(0WKmWa0`NrE_d%+jA%eCiULW!KcpJ0x#mlN@ zokEyzhR{u|^@SI%CV-nr;kn06zLsTy86X0Q z5TF?ZDOm>y2CqVJZ@XsKN|)D;JCtR|8%5kZHhT)Ca`|G_LxFQZd+>qaxjhWfdZ$8t zp%ZKeoHdeHBM8R{0q%7NJ1_*zgFv<31;`9;_csuDPbsE`ID$n$us=HY`C`B&VHhD0 z1=ku1#ZswsvHIHeNQ8lL01%vncl;CEoQ}Ul1g3b&GIk#-|tw7)$U6$M*L2+dg3Q zjY@)i+4JwziWvfIw}Vlvz|@#~A}c_f8^3(`%&p!>lgX(6GAORccYChhO?QnK^5}9C z1fl=~DQr!RQ-#fs0*r!>J~l$QG(a;@lkFQKI1Hi*LcqH1WL4}&E^!9niKLaln8BC* z&&D6SiIt?gWpl(uu#I{=I{Q2Hc%Z8ZLL|bYRF6a#%lpf_TU&41foQ@qPMygux#fU2 zfBt#FK28X9lQ%$BxD~Lt)cQ6Q6f-~sPnZl~!H0*#Hvb?7{aL#Q69W#zYU$6J_;in3~02ia;X-tnnA9fr|i>6c(0l{um$xtlwlb z-snif_pspl7{s7{?dJB584TnwLTy0s+yx&Yi7uAP7yEmw)P!&M>)l=W{`$p><>en& z_kO-8m$t|g-GO;E6XABv;V4L`OYXp*P&Epx*wpADzTx>>f7oM1v2omo+R6ur42M zI`-jsBCK5!0`7@Sozwtxbqbn+vJwi0AR~gO4uXW3XoCR7pgcNzQwY!wax?Qx68w|i zbN#@UYOp$ckF#ou)K@v_XW$}8as;j?QW`^7E#qTAipYe3cZg0Vv^EGx40i6%<+^Te zOyq_MLZp*fZyGN`xV4dHRpHIW{jP|&5)ZowJf4V#APCs=@%zEhS6>7KL56=2c+aAv z$9x;1vohG|xlsfNVP<}I?tUwR7Hw$uR(lv4eQS{?isL4^q-QzfAPCdxGELDD0eGS; zLm)5!f~@GKbv_jgPlO0k{DUMDg61H|co49a!OV>!Kviaj&=CX=hVnQ>s=EgH7A?*j zH~JZLhn>rD7y^rnfxw0=YchBu}4KME$)Zm>Agh!u`3it{Wc%OcES-#0((9NnA!_ zA2S&LAm?HVBDOe9#N#byfcxh(xeWqM!wnT0c%qk6p~2h1K!B}UkwH?F6elD~c@b=5 z<*dCPr_FLke zX#v7K%S<8Sx}(VDPO11Biw5V+6S2n{pP#U=W`?H;v}OcYLKf+hM&p8EO1}m{k8dFY ztg>?J)HMgekjDy^YIdyR4~f##6T_QFrwfy3`5GVj!-dm>+^0bR8%hsMOyF8=)^$kN zm_lI{^LI=PRD}W@kzLc`B)=lF>s4oELJqQ7Rln8zSXlEl|i^XC#AM~#n9y<7h`5#+9uIyA778a`2dVQx-E){w(kL@)D zuM#_#qGDM3Ad3#(k}%GfspmCg5at`XXb&l+t9iDR5Rm94lMpY7imLvrF9?gXKleX7 z!Hs6fAg&pP8BdAyEJ}St9~XP?$B# zhAK1=+-ufjXKL3^&uVR^pZ{&wDjgBnr1P?^2xGPUWVKQ%uoRs~5gG?~UJTO8fIN|o znKue^;zQCbVIPP%^5d4ZeY>+`hR#n9bEk?Wu z7Gt2tEF?;g9f1YdzIkH++`pdZuGfR~`k_1jHhU5GHGxB|c33SJS~J=7ivqQ@JLd>= zU7~ldC3v0%rI_0QV*pu-PntQ9|mEXt}WIZ-L! z#>q2vxIU(;O?tgvi+N_NjWN{f9eASYqngXlZ%MZE``Tf>)YF_yB?{#!Se!jZ1b0ls zVIYg-WRh_&0k)A;P*fKICoqzErAg;O$X6qRkAwh4O;AnUL6Gqv=*48|K7>T+f#btA zQ)YW?A!g@~I|+)8^D(axI5fTH1r(lFnoNNZV2+}1m1TZZ10YD0`Jr3|I3ebS1o{IE zFe{;Fxg$!3kpN!v^4qx*7z7{CT_OWbP%T|-hX6t%yE;1Gm8;}EE~kwdkfMlim7~cI|M#5NXcg6?QRGr#c7#0wzGglP2g;A4Q{N19Yqdos@!NVCh;S$_YFI zPb6zw0YUH$LjcCCn3myO@sRq#dk_XfqRF25wR+7b0x`Q0lscg)AUK-tEK65ueU*kW z*si+YEw?NaR&kfR#40f=+JQuJCW@{M3r$2;wwOa zTg;ftG%Zz5dT*@mb+$EPzPhmCkzE!_Lilp_yt;FGy0cTSK8HXFes#Fn+3GA;_vlAH zf0lgEqOcFG6q!xMNIa~iW1Yn$cjh2wNZh zcK#{|2>t{J{w-llft79D%$n{E54_XyJvU2~Z@8b@8BQDKgnY?4_;DiFX>~2VR?pYY z>J@mIF7`5*@q=~u!BKrO-@rhlHd$#%LL^drL-}f!c0gC8a|Q}4(K|+fX=y6S#wEIv zLQJN>2#CPYqs*)SF?T+%ZJcQwk0DKaNWf5>E%*;e+e{)2;k`1sQw$VM|O`-hM@+nF2vx%y&g2(#M{Atd^iBxszXAqIJ7%H-Hm z%nB%9Bi&*#$`FKQjzCohnk9FAkeVTa@9CSS@rTNC!x^jATnH#C<#)p4!?iyrh<;4y zPJddyTi-u;f$NTJ2&=pQ9)?B#w4+i)P_Yr%hvFlfKp>fvLq>u&Qp7^CO#p8qt?dJW z;_8$n{=yOD2|+&MeaU9d5$ebS07pQ$zaGC{AI;}~4rlR0dXE;4*q70+V+XT8i zm4qDx90I$Eh6H3d$yk;UBwYwfTFDOxCWKA#U-VGQYk zWMl7Omzx4G*ydvPEJ_qSF<=M^9tebhR3p@bG+DM53WPu=1Uh`-O{CU*P&~dSY$^ux zU#WbY5ahy4^7SMJ!7hFV$A^~+z+MFW8kca=b;KAzhWtvQ!WlD) zA~iyjjRH-K!~dx^f|91!#PN4mPcCg%4HD_77atI06I}95>TU!*_z2z{tjvt#{IIeA z=$M6KkI@9+*YCKaD{uP(9Ph?2ZU&U5<)}CoO4mgX!Fae51w6|ls0Q7 zB{k6~Lr{s5+87Rbi#C`TJla{C8P7@2?7clEz2X(SiXH%hCw&isp%u`Nl!pKyh~qx5 z1&oJuI%ozbRUg!jv6CxCdV7K)_!^?p1@P|4z@Za@GbQG_6bwDK7Q zZ^wuLuA{Zb9rVZy0WQXIA;_K~kaH8j?bk*M{#wd#jDQ#%i9YBf!qRa9zvEae#}O3p z7KIb`ghZH|J-@fMF+Kz{>$hM1$a4ckAXZI=AWIU3n?N*2DH-kb;i8<8%2o=|P{~06 z4D_~7>XxTe?EnT*LawhP4~h6I4k%Y&DuNF-qpp89yS?qSxN`)JO16^aYAFMN z(GV5$kpP!P0YNmI%_n3cQ0E9vsy-MuqlyjH{1$K^BA>-lAj@iE3t{yPg59&rGB5Wy z??#9F0V1f_+5Lh` zC7Nrk&TsuTRs`v@2o#f4x@>k>GGN*eeCwP5$)Y7p0gOZ;-#0ZPP$vTFi7$3FqELE5 zKQm!$ZAS!OH{#S#0scXDt3)5L9ghOr~=aShTmhIm)z=%g6f$7(pEX)X5A^ zeKGxgby^Z$QZpcY840R#H|&2*%y5T9fZ)yE%2+R=+aDlUC#->>001BWNklGZ~*1pWO9Ka$nqKKRKjw#67>kd#u#(6wP697QnM$0<_cH~MU+?Sozdweug;L` zEe$M_8$lU&Ox5*M!5?GCUoLI71FXRhd;Ai4EJQH`5adM-?~Fm`>Bbllr0)$8+(v=o zS)BDxP-YypE5(Zr0cHd&4^Rh|Wev(i0Dslzs{Sy7D>s&;qYz2z8vM3mgrGuIOsoUO z$}WWh5WK!T1RwDQ_uK>EfZ(XG%({p$BE>t1dV50wj}ho}_mCM3^di{+p8Anbwt#$o zeMx#BqPmiX6e6_ms*&eQOHw)AJLYjXGtP@>$1{U3ivPf8Rk%Et;?p+6K@T?pGK0b3 zWX>i~ZYe$)y@$+f=^ZlyI!3C|XaIsJuYxBAp0?8Y!}J&lWLyYt|E}OZmP*#H5kzw_ zF9F90xc3DFdb5b1J}w+AwgusaFIM4vF^wBCZA~3JYqFI}12Z&@|BLH}MqF*-QKw6-@WJW+rivBI`ZL2T@ zm>C$J$=9v7>6y!dkoj;T0s&{*l8s#2;*jnT00hlikLUUj4Nd!HM=8Xl!ijwksil@E2}P8%;&C$)!#Aj6 zF?r5iy6hZY;AVMxr_)&-9fFOWSx;srzVD~{eI^Vo(3~ax5pWUE9RkH-uv=@ge+#ZU zz2TQly%3WsFC8OLyPJ~Kw17b>9x))oz_kdWqG#CY(fL!e_2-?={PF(C5O}=tD_lv` z@8dWP*lHvk;`>QMEF9(X92r58Ie=!Z)(YBdudn;&0f~tzsaA3j)T@A?H?T}#kTYm% z3v!*Zci8FNN@@D(ai`O{vzH#3D{y(^*`GzB`i%*PlHx_U{P{b9$)qDuGiOW$f)E5% zyDY=3fg|wGyJlYsQrC9I5RpofwAt1Ncop?SzyN0#7j;iabi8+71ev#;`A%ndZDa^s zBHgX+hl0=^AcB~gGb3U6;FIWt5rAE$aTCCI>8efkOe4gjK6>#O6{2BURs$oDs%0{R zz7<$x^P}F7sB`YSed9T>hGu1-K_uXyf;3S&N22MnC{QMF2ek zzGer((3GzTU{>(X?lyZTG6I|JI{wp4)6!&O5YoL3?iUbC(`$!&BMV7)+}`+!Aoj2T zpjn1x$oA3B65ua^fDZ;Ez(=cvDSthDKQXmgyUPi@#DJ7et*9~tB~1>5AnA1ZV{isr zyBBe=qf3pwu0Ro#mO3a62C@}M{tC}&RqhC; zze0b^5e)EsJwU+xaf^3R%+J3aN8xOJ$0ZQq{`?-+V1NOA^s@GU2%rc! zL8C@RE9Q1)=u44FkqnqC1|OQjIdI_)f^c63@}l&mp2Pvkm`IQzgEZ+)+Zm*Al1^ZV zSdA3}r=1(qQk;q**K4p52SEx3$_xxQ4}$pChgoayee$94bE;#uv=-ZkX8+EA{nuK1 z@3U<`tw=4r{29ijtvp(F)a^)iUzR;U^?0~u~G)y+8kT}YQ zJ>M4^E_i5{3@lnI%O`&bX z!y^VV_(TxgM;X{@jW6Z%()Fu0<3e5b-T?>%u?T^^0DuxGxlCLnSJqvp&Rkvun34W7 zMkK)L)Wz|MH%Wsm6xqqe&%dGX1Chd{)kvJS`Z31L9EFLbnEu$`pzS43^$e z8U_N~btC{~nQifdKog-7jbd6FHV#fO8Q4(<Fe7D4x; zDF=$cW2BcLPp}smj4}xv)d235;Oe#4kbtJ1Rl~aKnxmN#Xh|3XBffsA5KV_*o=t7_Doxovju43~!;}Fj zf4-$>z(Zj!CNCR&!DacgI{{cR5kZI|C}vPj0!G}j)qWjqfH;B4pwnxQtD2!Yvp^`q zZZIuDza3?tNFeL47>Z`buC69vB8Vhq5m*Kb3?|s#=`sW$3|&hbKoPX-T2z`7HaYV6 zDan3tJ7MrUasb&9^4P*5cI<2+)qh1{h}8rn8Db)k5e&-*$A$|4Ect7yAscGFrW&ZK z(P~cYpz@QRka2Jr8Q4d$9rks3XjsyfwkSqj2 z0Rs7s1n}n;V~Ac)ErvdDoK90!kprqWFl54D^b=yLlBvM zxw4*m9*IPrr`A`dAGp2ee6O8*gm#CpFF{2RHqyD48BhSEF&YxblW&$k27^AQ(-#an zgTV%cQd@}>NV_y-v=9dLn2?>m3c(`TAY--A3Fjs-*iAP zT3MeS@SZ;mbw8O$06z*Qmq-v$27@wyL8JKDu^e<-d+4xQsh4M2*{4R({pdaBLWUbUfhNUy2>4ql`4Jv; z-@WbqT%683eg){CD2KiDS`;XBG$|@b3fM0dM8-hg9nSWHxcDMV-a5L!apoa;pYLs! z{~W5Kz4)@lBnasB=`9X7F@o+VSQ9&bQQgwY!ccMMj{vN5+Xp!PIPn28;zd%x5znPN zeIS&&%7LJPq=@(~EklbC7>y#wPv$GMG8cBNZ%w^)pOp9Xdq*M=sve(KUjqR&f@j2t zZ>sb_AY1}qFfK$O2nw%c;TZ$6cCfH{s6IH8(~it9 zoV-DJfYfBqW>5lcH#F)tfpdw)1Vjgz@LXoKF#kCDX7k3*?rs{#?#|AJOo36#W#+&W z8da4Mit0_w4lIWW5KEpEBs4_Pf%i9EU^%~kdi}vE1itTn6d@JyO?8#3T@VDR*aUhZ z2KlyW2nxh;{)fyPyP53WxhNE7IDEdSe{DOxV@3*cM9OXZdaQN&iYV8xTSqcen0&f9 zwvda2kbxMi#P)V?va)EBnEBWg1BAXR!jhGI3I)Jt0;io>>SL$HMpF7!I`+C{xp1WMME9@ zoztJ$0SGJ{?RA0>YOgFcs!=QOVE|o%GO0v6n(LYx8f%s6Q#x3L0k(#Z03gR!c!c1J zNn%V`SP)+P$NR+)Kvi9CSJJ$%TQlX!d}j61e!%1%Qag8;@% z{Nun6u6+5yNTY1P`FBR|W;4472YX~3?C+*C*|qQHe6+9C+;l=CQLD3&+_Ve=>l3Po+@DR-8X!@w!T$xh2?sHO5hIKa9|Y^(9C!6DSU4vkg8E~ zaa^0es{A9RQb0Tc2ph5`0s^3jl^1Y*(XAyH((Kf2epjl zC>TRx*v7Y2S7~o;IUF+=J^nE;HikF3&qn6S>SXfI@7@7|Uz@-MVq$S3Wc@q1Kp?&} zG(;3&4JPsNfM)?DU}!nnVF3bE$DO-!g1QfN+9eu-ssR({y+Bu?d&s`>0N6B?6P2#TeDu0DQgtXAc0(0 zzItC8lE)GZ(nAcCf=KLRYF8eDrn=okEY}kg^V<_Fc;r9|bGD+5I;u9D!j)&&5@LGcYtT{ed{3=+s-)6-D|z)-I`^W4DG8O@lN2!q5n znDQks(A0K-X~pI4mE1R^9YizfC7M|au_!DB_OelgP|g#yzidMYlRW2vIsk_h3wzLs z4J}26R?RK;5+R&pt3THDR0?8m$~ z*x9s*YkJ9m;1weDp=1NPSOyVRSlvZ-pDybZAI1S8N+(PxVW&;EBwZ$}9Y|;9wkBzl zluTQOiuovPLt`OM=tDN_LzdcR-@5<*Irr){YN94{Xbml?z32Sy@8kbJ$J|1pu!h^J z8yoA(%ZSH}2!j*^wp&+29aZuU3Ifyt{di#Chk&?FRo;9ZmWh!t65;zscZUbw|I>HR zXc_k8_TuoF(a$3miv{lgFRovgHYA9#3GIm06vJHWyN<_IzKtRPU7d=iMS?Z23#Od= z5in@$W>Rm?3CX#4ea)N_$}xB$_3=!f>Sw0%)v3Q!A>eJ3G2gpY(~J zjkTQa>ezcCxGR$V#6^hkWg}`dF!0x}Ze67WIR9WQ`CELZ`q(&dAX=6nY)d~x>6oUR zCKR~;#qWUtcOvkuCuWGyH5(KwxWgo>(i@y&ohFt_t?WF}f#XEyF?rR@l_w|p71u}>ZS~2se;%Ml z5&?R06$oIXkGQwJviLxXN?G*7UthikQS^;K>Rf^a3P3!%g9Hel?l`7uEii(xtqUsf zI(@V}Q0v*D4Kx}o9@o;iVXD=%$&^Z^TrP)KnXFBt)9Tv|-Q>^>JCu65y55T>Er2k+ zk-#Qa3pGopYlWZkUE-rtcn1CQerojTKuLi5uHGF@+R8uO;1A#QXD0i77UISql6si> z@a)XYGIskPeoNOM6zD|Dsa~z)00X%9egq6O9J>bZIt>V{oAHM3tksNDspSBLR&U@) zGuN@Y9w>uAwl!Ml=61L~;HUcM`N(+kF5cdC)Mx}sh`?!;M6|S89VnUnKtL3yEWm(BmY<9a z+ib%`=lh<0B|aS)A20neKFs@eo%^4TOnf)C&GRF!3_l;aCe*`MW&;5tLf7t}JOwd8 zS>s|9a5uFZ&sGjEb|LGL1$f|Do}pFR#?VpK2MRrtn58mQU|DUWg061Eo+_7GT`my; z8z|jI$z0mC2b>C2pYET}Mxs23(U=Vump0s zl^9^K0*u<8WS|(hB8m_=)K5-y*52e$$aGc9FAk%=j%RYH(^#nOFfY*I|4)_w(^$pwriC8K@iSn!vH}hK2KIRxk!6HmmfU4b?ff!KH31n z0$}t^t@S5Ac`@$$T|SmC0-N&rhpd?t15n+d2-R&R$TfOn=^8_!P!I=v^?HTBZLbZ2 zLd&uR+}uZ)S2^9IKI=LO&ez`VF5$Ij3zsseNX$`{ihk{m*!_gxS zhtp&Chv=o(Hbsb!+?uefJPop;Qyhg3nX!rw00~%a(}|fh5>q*m2Skj9JTMoB)9w!n zFc`IU#FapRLIhSiDt<~eIl3r_&!3-shRc7Z?)p;_xGwU1J{Bv!&BuzQ#0Mi@213@^ z>s7Z21n`DT&K`4VlyUI*L*yH@wFs)(I z5xaLgJ>Z84VO9=zkWR*1`&6L%6hZ)Y%ufnRM4pgtOAe1MF5X={X!(f~gyR6=Hb4-A z-~T>G5!mq5>t_OS>5btHdSLw}5LBRGBG+AWkF2vBqG^COuXj>MV97F$drcr%b~5^w zNCol;j__LCR+udSB&pKWdfb%P-dg8lRbAnE5=u8O#;1}MOH0R@1ILBv-->wzfQ)xk zg>q>q@AcKxQz>-@fdGCt%8$8fltA#~Wg9iBT6Gj-?hVkM-D~jA!`gv{9oiuftOprg zYb`$GSv)JgDjpP{hNJ+cTX|OQdg7{*{R+ym$^QAxrEJ*ps(}UHGk*9ezg=2>99!=& z3t<*zFxOO~4k1oZprGGc2Z1sO4m%RTTSM#No+EZZj%Kfc9UQV-q+?PfMfa|!*_LEY zgVP# zeDOZV1x;eEF}Ca4+H|D~3?dK!U?LikYBX&TG(l>{dB9)eLPKywFzf1HY?21HSN-wHtxb}L zq#*3RNSI6_2;Q^aa+wnHo5B2wv4g-W&yVw89zU~mOfiPV(M zJx;et06}y`lj%4l(hI%}zce_wkco%qlZ_L>mK=|}<`+Z~R2s9ugdmo?UR?d4zqI}^ z>YMT25`nxywbIB*bwH))H562eLkW?CLeTq^G00mmf>3!XC`Rd=xUY!^0Hql8INWx( zRV>#8QFsY{jFrLW9AGdW_uXg$gBz}RW?_kvAivG?MZkdIP~3$t0D=`)+aJW6-)gED z`&vN-2F?%?sM?dk4ibVCT`4eJ+u`h(KoAJ5vCUe$8uZ*NO+G}7*2Iqy?etUzqaKgd zUIIZ-<(Kgc&u3g2bOXe|^?9Qh*s^E@garBGE)U~_-B|4He*uI1i+eBL7PmzZ_AGw` zERdq$tdmW3EL-TM{ks38n0ED{Lg_4pAjh#yVyP(^L1`o)l4GMxG-YZq z47>MCwTJy+2udh;LTKq#HJVzF$&^ab>R}vNrD4;=LT9QY(Loi5(oi8oLJ5$@bShx* z_$eMyEejQaz-~8{W!ZWhiZg!1H$`Xwe&B1YK0qtBWCxK1*@$HXNigBVeS>?Z=S3g` zz4H^+dJs4r4L~4ZfHj)e*j8JL2n}YP&5O81&S`agZiYl6mGmIcd%u()=*8};%O>eG zC!ETgEl4pCAgEZEm#$BEesjuqadT^KaBgtnh9y2VYHP@fC*}wVrl76>B>2hr@#p#c zBa`V-5v0hp@349rf`EeHu%dKUrydQaD>j%0YVoWY#h983y2V7Lx5~1mBq*m|KG}2+wAj9s2kKgRJUgGtixu0SSvp2vJlE z`rEDMS2*T)Ml-m4qxsd4q{q3~3JGk!*y1_1y0W4~Baj3^%n(-I+NA2WNwEu-G(NJL zxZnsUlF4bxAV3+|Ph_B82TIZQ03^oKdb~8d001BWNklWoHXU2ES2&s9|( zofF*eIO&a}AV~G=53c5BC!;h1COtr{VD_1?09nCW6c|kB8CZ{DfZ?b)h>2Wu(Pvmj z=M{y(R6sFKm4HBGaLVjJt7GnChE`Feq^~;ZgZ8GyR&LR{;wJS(pvR&ZRMhf3`FuBo z&_BVm)mM&8j31o*)^)Oy5t*%UTJn(){FV_BsNw>86J%i5k46JT59AD#gInW13`fxo zC4MzbVHgE*(Sl(3iSegOv4R|fQ>DpTE$Mmu<6`0FwK(7ZOq#ZT&Xq67;E8aebf{Aj zgUTv)M!fn581#8L_I8npeRcmcMj#2oo;X|adKM3IQV2U~_${_8MUJyS2@Cl2uSLUl z^HWUG0>|2cdKh+lGCuV!NdwU^J#vFpvt@x{j_RT+u~a%UkM+ zR_e>IQh+DIm$XLOPXJrP#wF4wZ0>>@>DoiQ@c*Axs|VnoWELdjfWLz9l3 z*4rqA>_FP$PvsC`vCh6ob~Cw8-8HC^bue&HB7)Bw zLZb{p2WDu6g^KGf&OPxYptJ0ls6w2lTj{*F2d`3D#d%~mkbxESuCshiZKeD;(}@vj z{_ShEDi8$ipVBo~dpZIk3wxCw00dG5Is44uqfc})v4HM{Fc>up4n>}<9-hh(2xB-3 zZm2z(cxZrV2y%hczyVYG%0WY+{B)J z$I+uuT2n#%&&)|D@1(=BQm;4V%2lnw6^V&iU=2bYAGL$14^ja9ctk`nxNN|(1Z;aW zI%!@YjthYiA;4stzGjFib74Ozw@fA-U0MnNj5@?|o{7lImoHwtd>Pr?^m@Hp9+u;O zY@QSK_;t&z@_WcPrP1C!_&$n{eC{7<81!)N@!p=gp2#ZJj4sg3_yMER6(eX=8i14t z+wkz+54j`DNeMxEC!H2MTeAX4vdADx8Nf=|NmzuANF6o{)gagK@{vb)RJb%?5dc8r z-^g!`F?S2kCeLoqy{Fxg54JNP06``*bsybaffn~R49;-;g!+k*-q5!MvRq00P(kCP zQG28TNUO^!oMXWlp@E@6(Y0a9uMPrV>dSkmebHr69ht8}8I2O~m@+}yFTXCIncN;1J z7{OTnp8)%|?VGOLW1=bQMIL7eHhK&!nFoL`;FpRDm>nPkQlLNvBrXahmjyk@!Kh#e zib7zu@8f@JPevsv&h1q?&78*qV{mP-GtZ6PW4&!{y^rOm5hHGnoj?2tiu|VeEC?V& zc79y>gX0t*FkpNFO2C2<^vbSMMC?D_wYO&Lh#s6FWr76!oxd{Rcx6eO6|qE96$&Rf9_b51M7@t&>72uV=diKn%yp0rS<++_H=B;R zzz%?cPl}5mKbRu*qJ-WYTsIiPG#($9a3pUU{ZG_Dg=IHFNiB6i085ORG zXgZUm3jpw9@M+b%iwu%z?zw@4yOdc5$r1KCUk zFt7%rV%p_1JSEw%&ukW^iApcpt>HB?akOYJkYfCDm+D*yY{O6N_ITh2=P3h)E5fK~ zX?ztRRNbyTgB@J{dYkts@?tQ8h9!<$YLFR$7^JQ*!S^fO5MtSbjA#Yf4BdHGT3#^~ z^8@{)#K2)5MX!{w3f3gLAux_owtO*H_1g*6&G@W z(b~SfDY!;l(K5+}F&+&NO(+b8&XK?dn1HvZwSTN_Vjg_aIw={B6+}fdgHO6~Q`p+F zozWeh-Rdq-f`(OYexhN-C@&ZdgFbuJk4a;ndsjLGtdM3hDHoF_lW(MncA%^(#GipE z?^6b0yFQE+XcyBLk3fM{2|9HEW z(73TI>OkB{FJ~1D`4D4bN>B}<(8y?Ea0-&GHNpmOOf6<%BLZ5b)Dpevh(KJ0N~A=s z)P#;qrQI@QlS&p-iJP(so=F&(5C%8x#gLUJo2F^#_1ydPd-_zBN_OUD(}a$d+@pKW zJ@;L`SF|uaHy`?&$U1Yc*{3SJ_au7m^5iB+I2ar(Nvc~7G?_G z4mf?C51e|qu?*2&x3Pl*uh zMZLLUy)^{26094QVs+Bi!3R=^9!4)AV9@GnS{KS9mQI8FkOo~wpO-EEYB4RCzQS>K zya&~0T9ylFx9PsMM?+zeiUTemCaT11KYyDw?HNTYE(jk929NGM0}P-By8E1L>bxC- zKxNFD6oDhMS*SS*6&Zp7rwwo)DUk+7r>*W9+-$KVxHQ9e<2P0*0+e75OD91{Lt(pO zS?==uB=(>JUij0$#{@1UBXkIR-X`+*Cnx`Wx&8!c=+*PPSqM~etw~E!R|5!|x)j?* z?6@3gBqKEG8!+}=ZO~Q@dG1otB+wE!A3_pzfrh9g@Mwr1P`|gl?ee*^pjN}vW`6tj zH*)k$@bTSh3E#(qa4N%!79#UXZVT_+G28T(8ea#o^-vkh*n-spRIpwKfVEsH~zrquu}GGJ!$ zS1uGxf?nccADywWDHz#swI0w=lmt@4<=%{gfQ~-|{>P1A5demSEr=DVPEim*jLFvr zd#`WC5Xc$+40K1!05A=4V^STxC>T_hZp=xhZ4fGoc>{+t-n+%r_~O-663pujyA>;a zb(YW&JU0Bhs7(j5b)**5HlweM@${2bSXKD7O15KhjgA&+8Qr=0-3bK$1Sfw#eoZ4V zHvVK3B6wOH-H|z<8U5WT`CTXjTXe5-UKR!#R_w7ffV$K*imCA>j!)eLtuQTy6?CdS zM`*~V)j?;vI94;XV_FFlU~S_Do<6n|Eq_9!8X%Dgh8Hbg5MGf1-tQlLnL=P#^jG}} z6mJqPct(yiIDpqVs1Ma9!#Enp^1z;tNK0Oaw zsR8o^KXZ(I)F_*()JebC=)~oq*>N@C1Ue1wzrznCr;8cmgc7J~^4>ixkZ>2Dr8*-z z&N(`e8<|`bP|#+rS}yrBU*&QJ6m%kF;Q&GQ9RfOn#|Z?oh^JbOjm0gIj{{b2keV1I zXgmZ*Gz3URCVc7ZU!TNVVL$MjFyz%!RQ`sPAcVb0c8|?|83T?I}x<3q5 zWVK=qYIp@L!|KEY`3H-?TtEw^uku~WmMP7amAdy(Y}%FqnaJuQRU5m!(SocHlZ_%j z1vXd4&Fs^IN+>MIMj<#t0kqfpoMn#NcSp$K3U+))8 zgISKZBWUXbexRpH)YhP9GXnMdDoKKlmzg%tMNC9eV6C>4F9RYBA`nt21TgIE5W5xw zAVYsge|EA|p+LwGg{?Q7)`c3Q*N9lx-$m~?5>&)E{Xr`xoDQ!QFa}@D@!@_MYOL1D zfWWaEb$X6Z_cr#iMi7?|BQfwuXme%!Sxbq}ajG#8vXfzDF{#370LFj9S}Lz>3yxM?2uT=1rw|7qe*%QhDwS755NatJ;@y3n0SvI*QG#n6 zW$fK^K*q5xW4X@8_AiM&oBN_rgMxQdQ6#me!S`cZ5&ryeA}{1dAFDJI{*lKD|sJ9g8<;-MA(o%5%0zJahw1WfOsU*M&cJhH8 zl%W?mBd_IdENv_%ub9WydRsJOq|X z#TS(b!_vWrBB=orAwX5d6(59Jqy#

)O%HX`m<+jSOrNR~#uq08GvcML9G!RcKA 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 Ef^!6M6951J literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/splash.png b/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea0f9fd838ddb6e575e51619d2be2e925f70ef8 GIT binary patch literal 14473 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

$s({!5o!sx-UC?L8XNfFY1U4@Zm|`ZEWcV9 zVh>icTJ=QEO&X+@&^w0}{aCKhZQ%@aKO4{5M5>C=Dh@n){PbSw8YaO?8d;`T=C0N)a zf{_QdFmvcv6cr|2BjXdJ!}o^QlV=20ypVOts&KLpEVEQ{<{gNwW?f%S;1y1ctOSw; z?j1*)tsu$rH(8by?jT)AR(CWiXQNW&{*hd9w%XVvD!p)VvWPG!jD*+V(4GI!JszQ` zqe)z4xqatkl~P=?T(nFB!J<1X;O&I=(0MkJlCtXW3WC* zWO);;sgL6dtv<4Kbq!n!2aBXj)wI^wL}*}m5Y+3=Md&ll^3QU&2$+C2<(db}3Oy;F4K-?2wj>GzNy>hL5=D3P*;pq!S z>7PRg-=%?i0bf`hhd#eNJ{>1^YlZA8DOQ$C785NRXed#r^{WuAv$UQT7aBUfVI9Cy zfULk|`M!`r+v~MBk~NuJlgDMcN?|7I39!nsPXP=5WE3wJx#GKt{alwoj(!dCYK@GI zPlTQie3t(j3?o;|<~r)tYC)DP0Ehj`XkKv`@9>YUC)ouv=EiA+ql3*(QONEiju^;pLFpLa^^FF6C_12rNMszTw6pxOe#1=$+tc zP5xcDt~Sn^O(LrUWL3QeOCB8Uo*DgYyk?^G#gD<&7k8mHMj|)!x1>kv=gkz5r6XC1z6MKN z26E>Me*mNrY8oG;7q&~1Hj407jUq70@B5|hq=n1xH#ZBMD`DTcgHH*Q<)CE( zSjE#LK^C`KhifeaE20O-R&Qgnw4D@QLAIk}Mzegu1#90KYlu3R%-r*e%`PlXuWY=b zwXw0Wweq*W{^h4%e)({8m|M#i_dw7{(Zs=PPFF-_F$D#z6QQ21M=n^6KWqxc!dkfQ zq9xqBh~l9dnmYg%0Di>5qIWP_>5ifXChL|aOKCJ|KYu4I$nqXh1*}+zb&pJyrx2>G zH%m(|JnN4ymR>9^y&=Ya`T-C-LM^+vKrzGFdY3LfGSO#Wdw~6}-?ve*5G(ZWl|sQa zS_~|s;*#|~u0;;;5_0?@6~qRBr7D5R@?WFCQu@rq!Ef_z81p~vpt`>=36_Z6qxS_? zxg47(s9s#Nh%MBxt*w>mm#-Ip{PBlB{R6|+%L^J*Ro&5gvBK`O7u(Iz(?4%-QddZ5 z6>Ondh?XJ1V!0Nq{}EVihL+^D;+jG?2?QqV7A7kauJPgAS_|_(tyuN=_Kb8g=N_n< zTX?;)aV=E9)Dka@uvTzQ16;2cUo9-m&wihwL=4qy*^{^;OX2a*@Z%>>LJYe6KQ~z^ zWPPD+yOLEP_ue!r&MuGrTSJRn9Es&vtkawO#@Od0V=?1aq-owh57l}+3#URF|oiz46CD{i~0PK z&?trO5$!z)7D?9XD(>spMg@$;v4<*-)e=)Gs(w`d03^^TT(8)| zPl>EKb`Bf9=)OqASk$G+kwxS0R0*LiT*Y0TY{4)JqB$%uljVQ~WSuoM6~LMVuZ#4O9WdL z`>d|27Zer~T}-tQthE-qu4y7LS+_7*T8(1`-qm1aB`=Pp%NnBBo_+p=2?uOCGqbO^ zmaer_z|=G%1=!H)CX26m4%S@Edx=CW7CpYY*rl0h4p#l&+uO`ypH%?P$`2AXTf{P zQ7o1}*tN8(URM9l-ub+y@uzW|z3ideEro@>isL`v!IM$;q#!xX941)(Mp&}xe3Jyo|nkkdD(J?(uz&+~ldhsI{?4|AF) zW|B}yY5L0h{d}Hzo@dNJw9d#Bt13#?6K^+&TapFiD_V|Y#iHJSgK30R8P+Imw_4qQ z46G2N|r*;5Rq&g`#m@a`-E%Q`0( z8CZfeJnNl}1xnVDhF}G~B+D)S!m@YibH77i;Scs)0?i6)gEN|`|6wOQK0anAx8ZO& zI(xNxzqWum6@w}nmw##{egaDo6%_}LzxxoVv-8?uFg$&=RG1=Fh4Mls9slWf zhyY?!r%qeBN+j4Flkr1W!^ zWEtR#j54$SQ0!c)A!&%+L%7oL4BrSa@b6^m;P$qF*jO89<&s|2

BcE)iKET4tW~J;jBaVXIoHwuWb`lUT7_eQs1a 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 JPDHLkV1l045^ewh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/elephant_friend.png b/app/src/main/res/drawable-xxhdpi/elephant_friend.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6309d0f0e4c66826bd1f297d7a4b3434961a46 GIT binary patch literal 85936 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 z%0A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 LNkvXXu0mjf6&%HM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/elephant_friend_empty.png b/app/src/main/res/drawable-xxhdpi/elephant_friend_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..7efad541cd1dfd00da4d1ac812ba4d62b5667a29 GIT binary patch literal 115492 zcmV)XK&`)tP)L=00009a7bBm001r{ z001r{0eGc9b^rha08mU+MMrQy&4OqsFE>Y3X+~5*u-o|D3*3wOIdAzRR>aGc4JF6P+oH>FgkNiF5I7KRb+WwY=ZH-iN2$bSZICg zvwr2Pb?C2p$9-X6pQrQQv3|?ZZc7bfaftBS$)tN%IYd<6qHFKgzxmr7 znAw_X|NH9TwV=v>WSeWODr=zE;d3nKTd>OIek_&^VrL`ieajNTb5~I+OM0vhJB1+KJK!5XLygt zmuZ4>S@G1ycTfwkg?8)5AMaUFWfcymwq?MJlIzaqrK-#Hg1>M@fupPsW;wy^(|J#j}%WBA;|D z;>o$=#-D*&AX7;^vv6DI&c4gApVOLT(zm8Fuq(zp9+f&;S4cD0EUzQveDSAp!vb0RaL%F7r=+XR*iFndV>3j&{28q|M<<>*dm{ zhiCri?NlhQ?*ITG07*naRCwC#T}^A_$QG4s$yK&7yp6vw6Ju!lH)xS%ZDy~9miZal zyGu?I!V8oJg*tbI!fD zsyy$XN2hBC7GLEzegmsV&GX&8hw!1>yLZ%G{z&MB9zx>x)9=WuGW=f4t3CM9A%xIb z-_yj4uiz&Wo&R7u7(agk%a0X%`1#XEeXi*EzgiqLeQ{XJC)g3A6?^RX=mFAO^bpxs z`v&GOG0(Fb><7h@>`^Sv4(6DWkFqyar%nd{xe$f5!6F#WrxyT*r*VPK_ih1t18Nd9 zXFQ|otq#C~4+{OkckwQCWn1{CaE%i-D|;5@Sssc;||b?T4@; z0epy0%w23A!Cn}Kb-}{(CWMTGi@-vBeCTYjID$>Y8AWG-G_oOZ5Ek+Z=y~#*sGhMe z@JHhA9VfQpk;4xQ@2j5b0(UwgGIUWjEW9C!<~J8%3nLM#<0CDsPPyM-BQACDbgF{>%pCzMERln_+LBjyhIsEWxjB$cdsN*Vn zDC35;e^#Emf~ft_T8rpXLJFde^u7^1g59QIs3-%YVpbM>woI8 zi-gZ`vFO7Pf`G6tD8ZECf=m#c3J3@*#uOLPrD}T+5FeB z!R^s zdPp!xsnY@rTts99gyAACR0kU1;>2iw=D3Ld8p6476&Vnx`-|3s2*Q{`s@7GH!JyHB zGjUS1MiLiL+jH1_18bb>FZ?OR1^fRn2IuV}B-9tM-Y12^1*B?S#RX(*-xV{43&LxA za5CHk1{~-+VytjMM$lF3+;F)PV{!h&L>d>-1EgvJ7(f?>FNoj^0@E-YA2AqUaSC4$ zNf-$bhJ$=z)=mhaiSSgu;7KVmgW8^B)>W?;X5DOsO{?h+gLTYE*mka#<){GA%GMxg*TV9?mRkX64+>t!yQRlIHA zq-hOd^uTzVGc=*6#+|9}P~1B%Ze*G^(tN=P12R%tGYuQR0paeI)gfS1nO&O@VihnX zFrZ6HpLzz1sL3lrAhf&Hy;BaOx`P=X;)`VL!xR=4;ef%S+0#<&J`e=BIPQJ@Y4n^g z_&f~21&yI_UlSL`4suXPRdy`x`Cj=#;3Ap%P}}po!JJ<&8a`VKnMoLK*=|(b7WX~V z8^zlOHDL^ZFudME$jav03#_lINOK05?jzz8A$#Qu5tJ~l-eL3?L`>P5`VNdA;8xYE*iYMIS$nbg2KfKA*$Wte_rem#o^o=cas-mn5>p6des$3=HItrHJBl-rbNK92KzQ{}C}EIvsUiWVuZk*S%V;KUQak z+3qIi<~p_(huxEowHIpkMRZ@rWn`tK*w)2YOBnt{fr8oYR+CQz_Kd^0bi@9+rD0{( za-k|0R7GcToa_+B_3m}=^QI^j<_Hr(qC9?L6TvE zalup+4$a(dz{L^n!k-eAj*6I!isM{twMTSu?@CTZZ)D|STT^8%ZbcV}-*lf9sS zTLtf`E|PP_7lizq!9qB1s-P5M46Xwft&v5I!I)TV!P2xr!^aLU?y+S!JIzcuM-DZaSK1uKvb0J@<*^9iyz@+E zrXH?hE6V;nGA&T7E6Sz|8!`?IC~y~V!lM0);fo>_xDd{pWTgbgHRNjjSrn<%8){8= zKtV}k)O?eLI}SF~Ru;pA=wf#!aS@A}cNv$`VqAgs9uhTyJCwWR2n{8LRFk`D5A0t;3w2}a3S%OftA(NeDqS!cC3RF1GhC#W^G>o@ zdGAK&W8NMtJ-*&XG)tex%AiFzN*AoxrPbC`E+{NYk(N_@>KZNp7%xX_q-7MzV@?#y| zi_&1k!SbFcU9dc~@!YpW!a^EvHe<_~Nhlp>cPc)YfEW~_a3xx487?R+)~jVEbG3yb z45g%;>mck-^z19!Tu4}`gj5tgIMpErbnzN4W_hCWkoOtI#22cf)=#TdmI);#!N3Va zDJe2>YZ-+8LNVdMn2Wr~Brd4&mK)T5a^;9I0ONI7v_CUk+;hT6sEk^2y4XCe<3bQd zs;sx=HX2agJ4I7;XKF#9+02*=4GWc!YWnUM5GTYWXEKT}l0$x@8tTjH3xmbxX_>3e zTB)Q;TS+-(F$QzBJJcvS01KXw;)FrQ&Kxs_q5oR=Q-%xSE0dBN?$!Ef`=8BbRpyc~ znGmI<9I_bQ)WmGQB z#ZPH4l3I?r^3Dv50S(8=s3OsPkz~wTtT)@cufKl%{m14hPUvORW(=_qVW}49bYjg~ zzPW|9LbY&OS-i^^iIiDYrW?f4vDG&8b?{5F>cdB!?Ofql$|xqRnT%rI;_mM8@$uKY z?bE6d6u}vT4GpO-oJ!UX=k_wZvQ4ar)%9SHFRE-Dl&i*8?!TML88tRKN*TrJg32gi zx$nMy`}XbeD-}|iSn@xWz!3MuC?RgFDAm;}N(w2y3~j!P3Rb|V!&J%Dlxs*$;hJ+{ zO^ZRED88U$Ucut=+n*E|cboMxH%gm1Zq`7h+=iHo(ex^YKhr9T+zJ`RlQTKVe+Av%&SkUAw zOmyAC7z|O*Ok;x%gNsTUq!6-|LQQI+f&QW{9n~6HuDhgkvHij6;`{gS6c~4#Rh;jn zEEzDe9g9JlUEY(Ek)p)d3%2}8YVn1tZ2ng^hIv&2L+l3`Ag>a1G+8y(xsMI%F&UMs zY}9JAl`87{_dh5w9=EHM&pasNpK6#CNuicpll|x81_R^&?47@BTUi#z?O4v061H>_ zZy+WRLW*>0R4mdIYFwldDqg8x3+jT3ZSB&*%??B)Y6K=K7L2k3dMpw`Kj1YI2!vF~ zmMt_Gud@Hco^#Lr^<+DaZM}zH@U_W!W|A#6lF44keIJ zz4~J}UWot(vf=$>5FAh#^HDBjHj3K|r#M6`(zSZMTd7nUAQ|alafDm`%_5g0ny*zA zBgl2{7h8mbWn2zriA6!8%UI(a#Ax!WW;l_1RVWOP{9b{Wk}Q!S=R2t$(bi5OonO; z^J)_(yYT2P`k}q}`vr&#=I6C%kN#?@rom=v$g#jEs#>X5tBp#feK5>9O^eqyj2f3{ zG*zL+)~jHGu`Y*^EO|E+UW1d$6^kYnTra#Y@{>3)mj_?*B$i^dmUca8;}^b-7e0K} z)VAAcO?v0)!^3*JU8&~b1IXysr>zm)m!#9R@QjS~H30l^*$U<&gvYWk$|;VdTtE~> zlf1d2sTaAjHS?trM7`IU7&B$->x;+FBSrc}&g!Og-Pjv-qUEAmDpgAe#+Sk{Wf@?o zw57T3Jg9~5kStvQ3yw4wBQeNZ6krH{k;9k3%*%qe%FvQRsEqAG1tTnfw{;dUE|`Do zU)kf=W*9X&Pajh*3b25a1up*heBN>_r^#i(G6oXL?(T>n_SQ1hM(gBc^E`x$Me`V| zdL7}rz57np1;R3xW{da6%oHOQ955L3F(TRER5ZC>I7jEG7RW^fn1Bz^jIWi!hiTce zM#hBqeEYTR2nie*!gRy;Ue3wwam}&-qNTauVN@zod4lGGV*zrJOqP;dF)D*`tC?uZ8}`6)=PrT~4I&t9LyBFKq9jb$ zLKM|PC|Y=wVB}2W?d9eJdV^D<-o?dY9t-A<*;ay6zhHh|AG`M2qBsh(v^fZ(3PLWD zhy{FROZjTU?w7%oOE7pWb!HBDs$SH9^$e8nOpdIotM zRZbtHTJSI`8BfOG15P%Z&FAfL3VS0|#6$_(GdcRKJL&?}T)XyC#zi9(1LT57QG~_b zaIe+cYpJXh3WhWon;~!CjG_-jJq9v_V2gUfJZir3oC|-z2tU|V_9ANS9&Xp_r(7Quie)$%an40Dn;+OM-a9x_ zSy}g#=Xc$s9T+gwe!(cp1-8rbDPF>2XFA*gGIj_IWxf+0hl|ukI~Pnd58svdXk2n4 z8%D7^mEPaSp$n9YQYIGTU*68T$wWG>#EcGu>8Ma^XlUrg#tz zgA4plEw3k8S;z>cnGB)4dwTD=xa3flma5&>QB7e{O#&ByL@dVt0$}6^gQ>-*bqp|? z<+*HaB1qfTUTkT=@u>$ctTH%?A;eJA=??##qF$JSA&gYQyM#0|+dx#2&oJE!n^Z0t1s$rol*fxYAtb>9t&#f^p}S1rLA08+{gUFJu^1KGay0;+%^Z zxQt8;L#jALQjL9FL}{tQu#uRWYatB6oRZ#*Llp}kV+3+B6jb#4=ly;k$$&_TSH4Lv zr15^Wm?!_rNQxOR$92I4TM!rb=elF|((cxf3XX;50?3F_NR`az8$CqgVX`4Mhx!M3M@}*=v_McS*DGg?1pw104}g>cywBlPH{kv?d( zcInwCyG>9W_s1NJA%P*Qd*WMserNw6GEY|p(aa!$xw=Ba;Yj$_PkA^TRJnm~KI~Hf zxV9oy z_Cj(IO`P$19#@MJaFNNFSi}g7Z2qt*A`iLtAHbL;%9r%kMz=PWHs7)ORAB)xgrgI@ z=!4seK5E5*fC5hcfWR0M7f6Q4F1%d50~bOJLZKhBqu!IunBT-7!jVWMtl#}N5`fFJ z=H7Kx2TlQEMk&gXk;XImS~`dWJNS_gdA z?&RN-$>d~$uHu-aVtZWJ9`AQJ8t0sg;a5Mx(lA012LNKK9zGY#L znI&vCH(@cyg{pb8D~*D53qSvayMHGB@Bo?Kl;6hq%1qL>S zWV_#K!4iT&K6^&D?$O;Tt|abqj$?#g7+5%y!*u#Moj(6|fIxsy6t?&GPdo70>1^*q zDAm_o@U1zC+ZXoOifd|n1p_Je1A{TYC5IVtk226VBO}1B+cz#A%UhB#OIz=vR-jsx zlF66`!}Q+>M!fEbG!BBnW2sp*VHUo18DlxeG4PgdiA7?!2B&a%2s8i;;{pJ&-+`}y z3ix5%DIE7PlG;&$luSn7qOj+hN@ept;zC-If1z9a3~3QOtY|cT8jMIVxnMr-(Pd;W z=zhgsyFwG)C>L>Fw}?qDG7=JMmP%W?dqTugCKpDd$a0QjY`}l=On~vdR4y)%!zs}50lu_to6~VR9)IieR;i&h-a-#=zKRnY$8}f7k1r0JYl`AjVE&4?gNW-Z zG!r*)7~%Ux%|B-^D2#Gy?J9}|j;^9y5Ez6+ETdcofzh+%Fb{PV(RKG~ldI8mzKqSa zTrOrL8O?ONjvw{<+t0TB0YCsKZ2PTkzwf*elF>gF?I0gr#{0&J+K|e&Vs>erv1_?7 ziCWQX&L1iiBGN>YcKph5lz(9qd+@y3Y>LY!Q}u31Omt(c6jR6KLXuJ1)w^XN;1>JM z)nIE*?;l&j9>VY|pkhXPk=Vt~7u?%!cL#&__FI4gnXv7FjdY-7Y?Jk#LLkKp7`IMH zg_vGDeC-wvowPD;!%)9O-ggv9os?!whAv4kBVYcFTfnb3>xwQjdN=yn_3N~7_G>%<%Tvpx7c z7!2On9s4aPMq$ir7}K4-J0+x;O9j|4 zf{SPZ;)$ z$yXZ!6Iy?AiTC0&jc5kbgi}XKm-qiueg>A zb}>82=3n|@lZ>!18o!)eg#EM^tK42tKI*L9&4N(Gl6(|NCTRK0o&v+L9B|tIRh{O4 z7Bv}CFN(j7q6QetlWUN2F+XIE&b6g+{GN8H!i~wh&{Hp3bQDKyIg7yNZR!Jo!7a7TcnUbP^ z@~%EI`?U2gd>Y>=7K>_3DX}~YI+>IU45QAvvcjKDX7k9x0GZ%tK!>Mm35V)&q)iG2 zo}f%*ITTrw3$7Ud{BQGT=)`E}V!(_y)9pyC|4d1y8d%+vo;rL9WihbUNg7qRXj~Z! z-`%Q6U#x_jXTppM+H~=A1zXh%Lbo1)q04LCV1Sb$)4@-Kd$GTtMleJ%mGjqVQyQNS zx#a>Tw&%nkpe#WFkI?lNaQq}(jCUKG+uO1?DRz~XJ+q&p|By7+7U1o>KFJc%DN%xM znJRL7209b>$n9RKekc@VGLPnOS1yDh6$+tTfNLQKWZ{9?iYe3!@dE{;`MSxu*xgeOMs?6-ViE|Sds|cS46rLOk z#*qq39f_FK`T4ovVjpue1I&;z8nbn|V7u4Q`73=y2h+(wmDT8U=Mjl<%4SU8`WRO^ z^HmB*#m)-Flu!iY;^*q6R)mzn?gR#guJY#0oQG*H6ss^=BaL>{_U2wX93n90W=4y} z%v@;2S#c1HSfW66k|-BwKuI*RnhO++-(EMH=dX4*HaEA$1ZUdY>M66wn7MwK^^HUs zBB4BQqsOlo7p7uNTh^_QaTULNQEq}1j9MxL~tr<0t?Q?UKzj;Tm(iJl=Az==d1WSxc%cevG-_3MGP)d zNCiBSp{b*+%J3l|WoP7YxRGHo$scU0<+EMNioXlK8+2gA$3P_E~xYR8d zgPj@Sc7uQMxs}S18D&&~1e1kQo+P8AVhGQJ`jjw-=e$T~I2Gtza4a|%JLQu&Qo+yY zd~cuc&V7XQ0N8 zulI?WhLn8Le&0x;7}rNGhE-z6-R_ulyWgW+=pD_FZ}1zyj-kFSX|Z%mH7rPn!v@YjU99_I2nzVfl6u_^XrOQ3QftvbJj~O zv}3_Aa$s(^@59BkMc%h{gY-gvgZx*vyr7s?&H`yv2n*G0p@hS(b1)JRmbc^2M7+(vd5Tf=)a$N=3>lDWZbHNcei_v%&3(wg@{~qp{7L< z7q$c=Q7I}eY_m*?xH!UbYna#Ow{I)6i3hRZa%!L4LRxNoW7}m zdx4$oYL7t#)~4B-i~mS6KrlEL$HI`RVIca@+cq&8MB5>rWSCO@zS_qql;s~}QLhfy z-V`nvG`>%Lm22V?BXS@dwer85n~S7NF(MV~SbK7JJ-^+AZ&LGJX%pvoL~YjEP0z-$*W=clSVFLp0>j<*QA5?8_nA0<#c@x2?*x` z9Sn20TECc-XwSF?V0hTK$>dx9nq$p3$h`=?u`lJiW&f%O#S$3#dPHy$p)CNI=6J@R zr19+M=D{k4qr}y7UhEMSpcd=v>kr;;NiM=e_r32dAQt$_q5Z< zjVu&PwQ;e7Ym4_+W1bvu3PAbQG7DG#9b(z=JVk{FL{5GJQ?B@Z=*{ZQ%&r>~49~*ok|Ji)zJg%VQ^TKn^Cr zMb#`RMxs=tU_TWZ2AA_y7*|+rG+um)D=wnZ@Cj1Eoo=BQOAj6^7b8B*2_gRRMPb_s zE&|b~XXIW4AsQvtK?T)@O;tY#|H-0r;DRg0J20d`Fw!zmd(qqRHlVn)S(SNPEU(@4 z_U$m9J_;>85#hSK7i?~AY221=+=i)y`bg7aF*cLSU8x{ z#kC+9f{TOWU5M18U{G&+FUMPs6*Md+b}0@!)?YB#Wx?>y1|#*CW~jT*CzDM6Oy z8u;$?d~|zsfwWbRiu?yP6o#KQxP&exL`m%UML!qRZtL+G)LklVcy`cg=`u`qoMJw2*e zkz`bgRAnbU)!domP{}ND0bu;OE}4KIa31KGRD5V-YiE9Rlno^^iw}i+fv>-g?>$$Ml$kzB-U`HC>3Bo`_sOY?SC3ri1p{$9E8Mufipmfj|1>DktO!%_313Is9^pK~45 zLKe%)fGwr?3aA+z3=!W~gF7n?L?UG9MIajf6Hr)JP@pCCdssxIqC?7ik1xiAS_mr& z{`Rf+`MUJFZJXwbssRsRA*QH`FeSS&fQ#nen_Mqo`T-Y=+N$K@qF0Z%UU$(I0p>|9 zsP0f%XWs7$eS&_^T<}-l|GhG@7kgd*erQb0D>EweOf=;smpj8D;8eAyI371s?2F7r0r|O$O=3Yg7#3U;r3tT1MM_k|Hzr*E3K1CA-FY_0(s-_EZLF zWtskNn=xbuV*mF#MM#i=DJqJMcoglf?fQLSzKKPp7qK^VHI|pVa$&S&sFbh^jN*RO zg+)00^klsQ7E9&l;`cG{{jK}GXXcDNcQpPSxQOrP>(sugvX@5dRr`^8@h?;iGNfo^ z96z(&?H@lgS6Ey%%)(6b=_&y!Wl34zFBg6v`sWPFDc{5~=eUJqY zLX%UOLvFGjGzURpbGNZs2umLVp0XFkun1X5A2yOE2WbYxlOBAbZJ{p_W^8&5_Sg(9 z%hJ97Ku-I6e<`c9%2a089vNm1lbKFTef&P(=QU-9MIg#@wXo;>ag$QZz+CV!DmWZM z8nd68P%gr>#LZg4s9u=GQFSC3&u&L3kOG2n$u`PuPxA$2Wz-Zd?8ZcVHi>ZIb^ds1 z3yZ~UF4R?t5Fd5nn&A$DGG(4YOs*Ld?h_Y-D;f0Q2FAZ zZ>ko^dXqQ`^7E*}7)#q7Ar%$R9U*U%`w1qRV*(b!0$$H&-l-iJslvWV1U>^cVFY5P5G2VQ*!dw*CGIxvJ2ObQAca{5$2N$+fvEc2r zh_RSCDIW=iyf(R@46rR)^rzPxx z6FMZ<8EWU)^PU{EEWyYO7(WnTC-h?g$yASt_1TG4Q7HLiy@5^&0LTrn~EhXFrqEa&m)4dyeo)BJXt+D zLVs-bxJ&QN3PUEeJ&KD35sWh7qJIBJ6iAIeTtS0TD2(?HrzNB?5UP0dNOeUpfY{HK z_|<%i=`b{#S+`YfGLG^YnrrEzIi?m9$1K&ivaIIMZWEh+Uox31=GbAJg zX= ^PcNlfswh3h>EfMDCT=<%ZLTC7x8Md7UKtJO}SuHgu^cyG0X+t#)%VBFpw&Y zX<^SiA%$$9C=w4JHzR?Rz=h^eFth~}4D6?8PbCSbn{gPmB*jtWk`#N>Fcq3HlzP%h z(5a;V)a3MRxL2uEKnxzHX!mCShl#i~PGqj3G|95QzaRCP>4*yy+mMu{0Y^B|6ghQg~ zMoqM_a9Uo!CUX%+U=TUTT7eN2tvC4Ey{)xncWd8E0E-_5EZ`ZZAu>L{)?#9QHY8|b zdSceQfm$TV&cSu(CVM47?xlJ20;moh<|f;V}03G zwV-n#@puGZ7f1FY*Myaem`)3NA>J~GR+|V^gu{&*Hr`}OEf9=9A%jr@T%0GSc1Wq@ z!`!PnEmX`l-t%&yE#htvSJoYn-K6X^2)$iPP53GczG)n_pZHv!EOXcB;A*AYpod}T ztb!#Kq$+-#XXZPYxAGV*?$8Mt$)taCYh!)MEwzX`x#x)#HIy<{WvyPS{dsHvHLS4>{r$jhHHlJiH7#FHbYd|!4(Yq#q zAwD>$4Fu(B6F-@i@){J3W7w08PKvP}JL?zruE%-S=@Beiq-&#nQuUVDh$Q!Rww|mn zJ#dHZ@>WE^B0I*XVsV!Cb6Ku(8&>TKFTD%`$uQLmEExBAulWTKj6&jY+O8b$s{Ctp z>Gvzn{i*X{;q(1IxloK;(95+14E)C|qc#u}M|F;4lMi^9?EpbT-4G3ZCKYVIIS-{G zxYyhyC6yT%s0hZ1ZukKk8*A$;%S&@wj+I>riz9OO57~=sGn^KOtwQ4WE<&N}v2G2U zDzc3e>IFOybA1KDC>1)Ci+!_^J+CJIT2<9mE>y>TgF;Sf(Xg=h)~(u7j3t`Co#>0f z|NY7)%SXL$#!R2_ShvE-08S&Eo{a@{B7$P%Ey@^ZbdUF>2j!3|>3_Ppv$M7RWW{-0 zwZ0Q1bKI!PeL&7(;wF%4PoBXFiQl}8MXwuUT+ns@^?P8v0T)m)IwX!VPe^&SRdF@H z$F0vjt{3Xe8GiGW)WSJxTVSl(lMHJx@U!!j#!>t4n^sRe8(clERN6J&9u_#C3&9{- zaAVOY6)oKjZf=VCY%&s@z$1d-NM`*{L0sjC4bPKJ%o}O`IKLz}|4 zqFz8n>9rfWuzrvpF223}`TFX^>BYs_d&?wSG92JzA4!LQ zZ+Clb$(~pUT;%!}1-v)&^=nj0%(ka-p>r;3&2E_V55J%WgB-#bjZl+RX?RF7wPpz^ z)k^|-ukh=2-H6U(7PJM^3DwOvPaz$IoDx#cAn(c#@3~%7!c%>8dIl6cej-J2SP1)g zJ9-|0bEEm<;BG1x3=iPNQ!St!ML0YJ;~=Bw@b7G|E!z_dibW{}hfdMOzXK#-!)$$lQ8W{}|_P1Krn4ysfySQy~iuU5!K&GU>e5EnLXmKz>< z+iZ|`f^7~#xhXGN+D!GLqEE-d4i$7yKKRG0?mvEgH!kM#5Q%;(6^I6GzA^|85Lto1 zDo1>WKnJdHSgQsQi-?Ftu-M9+kfGD_^8-}9IFdUeO}IeuV58Zrr3Eff34>mUc|!yP z=milBvvb8>Az%>}asKa_ zE1$->Y%r274*I1N7$gN8zhGdMIRU<-KLFy9PYS*Rx{aM}bFU$Y1>zzyXhjhhP%++} zk0Z!-i;+`E(hi+!9$esF>Cx^NI^j)kC&3Gpkc=zH1)IQBBY|YJb-%AEgM}0^KX^DRX@~ zE*cdck81l91t+gp9la&!FbHe6#X;CMi=+03-RVKnR4DMW@6nfoT#`U>5^#|TfsVk{ z>DhU8n52FjSdDE$L!fM)^2Hy2w&Wl-K zks|<#R^Tz1;A~`0&z@KFB&I^8KM2J{#heY%rT`ZuM8@+7v)vGfP-v11++SO6A6L3M z!Gv!8C(BQSK6hQPFt$zxCi4S-_MZmB-1)YV3bN@ z^lv^RBtn=70~e$%ta*G)%mq!e!Jo5CPK!}4pkP!JERZru)DkXKPK4@SD~R&D;=&LgN-AAfnB15YE2pGukNZ$Jex9GO=CCLc6ut-_5b zydx{sYF%7hlmHvZb|;$x5++#S!%$5;kD{|GB|e)(Ps9K){y@0sFfP_ixmZ=i{j@IX zg)&di;9}*WoRzbM*izG+bHN%yNFNBSOnocck_-M2QD0I2GDcH(*&8{2%49N`-vEuf zVv^@?g&Dy+zM&@rxM*DfG)iY#TGeAnm~jES>nk;}?;PtQy%a+Nse1h{0E}v<)1k)O zBvFgFP=t-m?$j@YO$y)Z=$*>%m5WDlX6lRc4<6h*NE1`wH@3dXxcFCDpO%aYAumxb zHyX7bc$Lw@Zy*)7g(7Uki{uBTlmUwWl6O9@ZJcQw7h6j+rsvWuh22XJi!K2Rj88K; z7+vfNqV!;}z(&?hLB=?T9*k`ktc!%*Ah5w%wl+;oE6N6uWx+bc649E~#R69$ma#S> zYhx((|FG}#{vOSYJ(4DqWb(R{HiyKK=QH2ud7tO`J{fOhBLSk(eG2k~AmU=Fvb3~W zSvtk(#dmr-VmMYqCx@DW3!s55h$+&{yZo1O7Z-P|>SwC7q&uLvG~%rUK85UpJm*7j zoUL2yv!y^~L*KL`1anct%e!s4K*~^r#=&a!;NYM{PL%k z3d<@ylZmb@ymFd@ss;(9%x?L6$42)e|y?Fj2owg67hzZKR zeBD5~n4cFbTFI5v-y!#YUPoX|*?DmWL)61d(s#u*z4#Qaqo_l*Grb6ix%a+!#El>- zkE6gQ$G`CQe1uUHkqe@;Lyk;G zl>xyZeITEU`>3`q!s#MPu3;UL2@0eYn2Mv50uEUWRJ`{CiN+o;TF6`^AjL>j@ZRw0 zQbbD|?xQ3w$k1-p;LoYCUEbiaNadnP?rM=>JibC))M>@r!ulXcdNjeI*5Rc}uW0x& zjv5wM3v5BckmBew8KhrX4bbYI#6<_sO~e5S7|ttMPyySn!4#u>jxGC8w*BY>qJof- zF@Hx`FHj_vAbz;HeOtZI4$0rMn3g=8X`N=C=>^+dOx>i8uIgv?IyFDA=rj+JD-K0k zM(ol(E&h#Ee_|n;XB6678EUhlkP? z)Ik+iz~lD7#~V2qipZZCR%d27~1o z)CJ-3)fob#Ha=xrz<}jfifD~;tFGtQ+o%xVDQvu&b><^C*fIrX^85#WA2oF>igyt{ zysAUV0!S&w$!rCB5MJFY6biXqs}lP-u)tjGpwv5z(~G;a5zU&5IKhH;3yPr=!bMRm zW-ao4dxVSM&wfX*7pxa|EaE88iy*6byLeG6-J7 z4bE!1coN9^AU3MlcW?J2<^~@NHbQm&;r*1G$D| zr^rqJ=M9Tl`5sCD0yfjQ=M%EdtM z3t}o2yhG!tvD!iRc6WAD$zrJTIZ%Pb0#PA@(SOK<@?p;^fhfgjO-zU|%3w9RPwJPY zYBAmY5m5I*a~Q=2m44WIj>tHxZ%n}+408cP2~smy* z-@pZ*YeitpZe9L$YwN0hT7x*sW&%T%eAN^;xd^Jh5)8VN9>26rtpJfgZojdPxKLc^ zG*&4WHJV@OhNPRh?tw8Yi=obc8|4SUNcAEZ7FFnkcUZ)Bp`9z&h*;Q+LbI^g3T0{f z-_37|aOJ%!Q+*Yv9ip$rbFK ziVJ33~=c>~sZ+db(!qGODE`b`FY6HF1a&YF49i?OdwpYjlEkWy|7f0bUTV6lgDm@;;pgGS{nP|(|afpLlHCpjQ+R)mu6CdPJ}(wi`F!rV7=Xs-I@!&vz&6l zM%#}s;cPu`=4>XD*hQ__yX}^xx$oSgNOnPsYvzbZaokTk&_^;%xtL=03${Rrcgqnh zUX#>9j-hBE)ejf6DZ^tb!r{c;1Zgqv1qEK;auKp?v2?N_N4IGU8{e;alzDL7+U`0$bpaS=Coi%3H^&1%-kq7=#PwcTpLrJTtLKguOhmX5OxN;b5DGh@`k&&^YR4 z=Q?}+^!jqUNfz8&6!wG@CvypNJu3H$T#!a@6v%`Odv6?Zp}4aIn(X9c`NjCS#06h> zgnsNtn;QrVky=P}P%fOwMTS%`667o6H5TE->Ga(P-^J6tb3t7=^*ZCiqOd3OZsq2_roI9x)jwRQ#BZOBUs!T+v#Wti7K}!{NXuMsEY==wAS{05 zJj}?~v{2Y{dQF>Q|MDCOo?c9sdJ3ahw>d|I#8JAu;p54_I@xUY*>!fU*&%Mza#RXyw*mG1K}E~s1xE*hP7_8CHBv)P)mp4g*$!KLv*eV_mUAOJ~3K~!D| zxX1-IDfWe6P%h{~?SR#y5XIobWsijmABUtD5whl;-$gxn^7_rjUh8yiDaCP^G34IS zWG`5F^7JQeN-sL;{txenk+^_Pki$+JooCr4%7uAi58G=8SSN_IM%AU<+aH3#B+3a7Y6#B~u= zA-y16wA+t?24kKKKB*` z+6n@F<>iw?uFz^CETY|ElvLQm!%6boUM#i%C*}SxEOL2R?=qK)j+ze3FQTI}%gZyPQC8erNJW=dR~HvoSC>bV zbFqwfSRfP-qN!Apos?nBHny9aXs0zdxX?9OFEAIKPWvR&Y~^jZ2&#NvdVpWnT5{2E zf)VhAUhsX|H@qfjkB4dwqFfCsRgqsDMEEXuBp0v$onOiCH6JYyxtNh9gR92O>f$6i zi--asVCeTH>PAIqv-2 zX`gd0zO=oM5*3U9^UrgswvwAoCZjk~{zM zBtN}^u!tcAkhAU?In7v|oE#Y;2&^tA=aQ?F_t~Mpz8YQdPAxDNgbH3;VLd7+z38O- z){8XTOHN-LG8eOm3v|xI%@%b&P)&LPTnILZ+&i;F`c(A7a2JJhG8?r}#Kv;TVMW?@ z>(W9H&PA;%aq;A2Wn#JjJMtEiQHwYV&hp|2J@>IVp#SGoNMs~q?_Mq>EC?7;_zww; zSS43z+_r%HhMw9kE<}1U|I6&#JWfO~rJ(Su>)!dYg; zGi{N!xyS{XW3B3<@8@m)?sN`gF_#it$Z-^eP!bm-ARPn==@&5>Gjkqs!GkDzXedRh z7rB43+Dvc7Zn0gH6uEVcry7`x{n_KU$4D`jV!IbPSeG#NM^l4TXqZMiau0}0T=-)( z3)2$)9}@Juw*QFH^}# zz5DEzlVOw2j%(#y_8VK3%xk%iC<xEM4bLK8PMHQP2=0+wVW-AcPHVi^ka7kIVr z`@BETubAiw%h?yF&G@UDoX`0_-{*PW=l%URy>g-5=xyMFY{q!iLJR4|eZqxl;~+Uw zK@~5ZZNt^X73A#F9zUr_LE@C>tAP*21{t2!P zT3aE&An!;@u~fotE&>L3A;^Ha3;{R;;On(s^+E>#Qgg%M z#cx-jPJQ}Z6fa&FxM)R@UK|Yr7kC-Y@Fs)Rq=_JK(uvJnIC>r@OGb627lWjCKpD`7 zQMUT-Pd{gRQG?g|hYxrrUwb~mS~}u$)MRkc@ZI{b^v8F8nS;(;;7>pv!#ZIbI6D_{ zfyD^CT?8!1M8xpT#cHq@!O#sP9n)|jr=#v|Mx$6UfQwuaJqdDjW9OA7=TnyOD zg_DojBT3pWQ)=XwRb)rOdO&OpCzVe=r~cwiEr?#~wdZ(f#sO*9rdJ)hY8e@D+1>w; z%YE3BFqk+I3kap4lZrG?Nv%tB=sTmx>{R{g42-zBU@*N1%+LJ2K=w`aq!ZGhf58PG z4+47e2xEcl#Xj!9LR=J0xNx~uQJ|F$jI;IHayvLxOU|^@{`ji9W6-E5h9oWg<+kf* z`h@8P;vyI)88L!)WU0?jncd;1GjHHh>bS(q3;}s z`%+1=Qod85p#ejKMU2V?{^tVW#Xt1Mg-$SFKZ*-Ku04 zT{iB&Am3AUu)iQJAnK5n!?sGp@q3Fnv)MMqa523o{))J$Zr{M?Y>_C61X5D#T>vh> z2j6Eyq1-J40eUcu@67OO8(s0~k&K`9l0ZLI{B+*iys}6CC4^x3!J|UZDv%l_t>Yv- z!unu5=$_70BXCI5NoViX5fxZ3fQvLzi{hmV`i)77stcS+NzX>TaCT{rZAh}LR2Zaj zKyxforwx#2){ComYr(>HGz}@*u(;^-*Wvs@HXRBr?M)aF@J^UvA=vOow8iKcqCzV% zo%*Q)hAkCh$gM0HB7|aCpg?MU&6}I_ zo@@$OggzX5&9uR!rpF%R|4s6_Q?Md{g;r&(`l+P7N7`uyJNbOR%~pOVJU1;h)J{Y5 zd3-Luw{kCN6-ZT$XEDQhf^5}ZZWn^wQkZao$T%7nTp0Air4I24weSkMl>X?2gSjyN z=z|?*&*|L-lM8nr^x}5CT5qLUMbBy`T6+m&5&F?@t_lvtgYEwMpH{35M)gV5e9^-;d6#)m<^pW@(@o;L z_koMhq`6)UsoY1xJ%IbeIp>^%M|)fxvk7wqD=d z!e)C`QVYHibu3ZxWO429rgR>K9d-78TNN8RFFOcp0~8@v^ls%p1p zDt+O?sezQrweD z2ZNsrBPj)(%FJ6qXc#ps`3q{h0Ssij5v7ca^M*J|_i9Od%WUJq?btxd++C=h^KXiL z$PNvo6n1y8;b~B)UKC%np1dV;VTDC_@%rN|f&#sz_l+%s9V03BLBE*FruE#~?~#m= z>z`G!r=@M%jEwL?a|*CPT;O1;(x{Xd4hRtgHwcyzB|UGpB>u5&at zpVX1hX0y~pgH+IHFKUzv7#D}Qm_%6Uxlr8t6l5Rtg4DE}2!=GLxd+A2oIRy~KJ9oK zSRC~x`u+;Y#k@5Z8>?wd1#n>i!ia`h{}dODNYZa?7vY~Xflj*bs3k?_8 zUf^=~5y&al3q*#23ztSNl>X?2W1Y8Ztm#p`P&z7xEEf_dgEo;t^HIe&ts8IfaY9xD zn8J%UXI67WF2-~3%6aBN%!z#0N4m7oA$FGPDRm$Pj?wSSnm@ zHbE}{jb@YBj8dZkW&^KE18GS+FdPn(9xTMg-qGFLKd%LQ?W-jRsu{2VF2G)(IO;xd zu|&8q=*6HMMM;AzZ03UGbsWisYGEQyjvX~SFz|D27Z@ZTh2p3;$X$3|TSZYfH#U~i zA;E>w9dssxzneL4!o`F|LKoLDQoL*}=m5ot#TS~u1uTTd6d3TSDZ~hVRE9tbJuK}E z?-0=ANPJSq|GE1xy4@ofe73vfx@mGsRP_>s3lCO{$#GNPP3sqfERJ%yY(2D?4$E<# zUZ@>@>Jy(DHaCjrqZmwQe;PiedXdA2mFDsmQPlN~4KNpYc@P)>%J}_WZH4urn>?)H5hyYShpvx!|#uRKvix z9yg?|&-8P_K325SA>2r7;V!^8BNb2}i?e@dNmvDXeur&!7mgeEFZA?C>2+Fis+a)V!r(QfE~zNgg&u^@By9LR-v>ju8E2bELY8$|y5>|CIQguvrjGOt~PuYdJ;m8!+$ z<;%t5_)_4`#S8_*Z{(vR8Ga)dR%m3fSfHRw?vPC-hs%J42%`uRl}Z}FH7Xd11`Dp* zyq9WvB$C2lyub3c-o0QjD={l`!EE;vlBGRNNL-lcg{to}A8_@73uVylz!C=RyfFEZ zt>-qM2!=g9=jWb5{Q_Rl1FIA*ilRa^ib8S`3L(9?n93xRFrC)#@0^^{7w;fbn@M`D z3xX_Jiv?rd7l}j!7`|k(T>`z}YS92Fva7qhyQ`~97>#So#EfIX;E`bfeWTRz(|arb zzVf`*H5j9O!>X1GQN4&#dlAUz0E@{B=X5WYoFo$AFf-mZ%&DbUgX%!RP`lmM-R;vb zVAJU^=Rq!>;a4=5512<$U$H0(%f;nCXG7VzW zhAs>YUi4p>t9_sM$2spgc~5>cnPGBTC=_jDaz5w#e4poepQnN&ve)M?xRNQ5(a~TW z1zi05Q}B!Zi^Jk5Y65amnq1sjcqgZVPeCvc5Gr7yDDgUsMnx_R3Eu8F=*9A~M^V_+ z8>Kt9f#awwPB;w!6&VTzxnhTfVv%7nFr*8HGps0zioiH{wYE?*7#bJP$lh*q^@7<8 zr~fn|)C=H3cb0m!8*m_ZbYp=Om({XKt!BI>3$c*u&eU<$4E+8!%EiaTUf?jwFOH&+ zT*#VS?7w*PPEL;@F3{6g#ZV|pNU7q%`J7Xn4SlZGU=_rUw5m63;KF4(mt@onGR?)# z8Se(U(CWVIY?7muz>qAedi+qKdckN)VNbfsu|^^}}ITmTq&SrW`242Mt?A+p^*8jXbGm8Pm0mc?K$uq83s ziSq72UkJxhC-Z+hQ5D=hc$&wydWgA;LlGD56cr4}OK~#j52^U-{_$-}2V-+*=cu=` z!?UOv6kLR?ri4QhwBKOkS-jFT-6XdA?G;W`tV?g@ij z?0-GeHIBIOmKYaLwL=jRwY$428hBJ~2zNYbIjo8v0~rj!V*cd8!)0Fx{XZMH>w^_9 ziYd2{iD82H~u1VjDhzvt=H!V$Ug4`BSqc1446uZ#;CMh*VJBF>DL za&f7wtf$Sex0^Z~kmt5H4-2W)^ujVrpl@{kK*!bS3;XB_V1YhSSrkS=6t%+1#iRAE zuJPG5*?absf)Ukb2m1Sb-Q6{yaW@_!Dei%4l>GcMK7~|V6!nKpI`wK3-}*~%gmv{OaLd|YK-XY^&K$%GVOW;-O^z)> zPEjtdUow~VB z;>CC;;bJj@xp?|CqR%22QMzlo)DSsM5sKIF#c@w~8Do*lJ^n)RMe#X3or|mm7vQ|9 zHP2235fj6~-CaCkD(F!u8IRM6)c>$b^d8}uXlJ{}N-!uFy*d~Fpr^s#YbR7F7vAe; zytXrivF2caY*jRFO@y}{JvSH#v97o+gEaCzO&gE{DPVqn; z+=G6cpE^Ck8^)K<6N%JMOVnZDfd^ujfw>51dO`MweQ64#{BvocUf4A*I(V0Cw;QsH zjbov^ZbwUTAyw<>s27Z;tnhYRxKv~7z=(1px@)G z8sWd?<`!lc8#G zBykV&_47c*L~LR!{n(c~=BTjNi(LQ`_>?*4AFd-*0vjITih=xV02LYk+ zW>cF3R4$&a8nd)ce@SL~anYm~l0h$6cxC?;RwlKf+sx7OT<~gIFBr{vOQp8U{~0t-gX`MQ&baVi z`AEnG=PPI%NPBxTxv;mKURY$KBrcLNtO~330nzx+?vO6yPFh!@Jd7F!)2++J{?AXa zTp%vqB39057-jmV>dD%8h#SnTMN)UW&E-8hhfM{DhR1*lbh$H$2PLh;1Mc-a^4+7O zqgGsGVNggupVhe_CtaZ*H=k4IQ(ieSxd~t_?df3LG9PBe#zHd}`am_^UO1g+UNILo ze)g(eFF>`oHrAOZ>Y5%QWnM3-cOv0#Lq3L#S(MH;S z*Z%8ouv~O@b-hG?NfJ`!6PBxaZ=dSnc3g~<<)rSi->f!{w{HiN_wNH2v8_a^bj(g8 z!7YAVnw}005053M5gVhoH9{a!3!_5t#5HIf$qw*>f$Vpokl$4vC%kg{?Y0pNQz-+u z(AUCHE=;V^(jU?6y?7us#dfZzIY#bF-R~l7qLZe=9sDlxz(qV z#2H~UrbkDoc`nE_XWV9(H4YESZy+$Ha?9slubdhT7{ORIxbL1GZjTtnf`w7<_4kXF z^<38sd+j?|E45_Ev`{XZYrJh_O&wkO+oI_xNwRh5BRek$uZJX`40`|f+8b%FQk&D+a zs9Yc}W|8Sg&xz~>4UT-)_j%ZaQbef)M%+XH2PznDckD1B8@M>;PmK%*$C40HjY1;QW2}(A~D^>N%Mk%w}S`AP&74q zmdzHCT)>#~V!kk><)IiC$CQiGgA5Xk^iNCVG)C|C0d)>Lo4tm4oa9tD<>Je-FiJbc z>XNT^nBHWm?6b35VbTB-_(q~PjwCr+f(y&DdNwAl9s0w=Rs{fi6JOb7FFDvPmW#tQ zlZ(&3Vse4N7@wqxTFYH{5yAOApU%a3n*Y@2TGw#4TYr9KdL6qF~^MEpZsXcfqh(vWq7-*vfLyQdy$G&dsK6=0aA-g)1)M!EPcK zTPar3d-8IY!{RScz{rdU(@`85A^m;_DwL?jmyd+RVEB{~{$2;caFa;NMJAa;hX>5e znKCDtgUSDscXh9A+*x#zMjokUAJea}j|;^%Xel^;lkrRRppVdG(TfBraq|!o6s(xK z))Y^?%CZlmWE)gUW~tpc4oV%6+mdWXK{!zi2?Dh&lPV7ulrHS1(0`#%`@6sI`LMma z9*Nm>9sVZEmRaGj1tAgrXFXK z2yTeAtfs4-mhC|=*J6?X5WCibgoIYtF&y<@Jr)1}AOJ~3K~!SXqX65e^v)a4PB=%S z@ST{a4^N1jNxR4>fs2$I7qO_~^e?3Cf?#1mN6q1gBewR!O&Gfca7I0PDH`=P_BR6J06KQblj@nicy!h!j$mGk zmEHsxEpiS9QHFE8OT%Uwr7m#kIS*AvH4^)RR5E=tM@^4GR!QoG=4`z#LAp?ezhjqMtP2LF`vIsoI=L*Jv6Cm4_Lqk7Ap*(k8PS)10i#ws;kBFw#r zMEDt#ApHyW2Zco}+`!l!w}6ZhP)Ix84OCr$5y9QIj~;y8S4I)Kz%nY1P?7xBp`xH8 z^-&%Q@7dAdo|&VN>zU@LxOB|@-Ccx1ln^_thbjpP7&2X0Ko@m+y70`VLG%>${j_HP z1sq=nCFKPMxZN&XJo3t@(IUfzJWd;N;$nms$OsrJPsR%h7rgY2QCx)D+uN&^On&%C zB(ig~s48yjN$rdPyuVl9_LEVRE;j2rz+z(~`S~>%7|dh@Fre_R&jyJLRHq+1wRj0w z1A$`ks85kzSgK&cXmpx383ZA?(4et|fWhG{(FKHubX%_Ewem!rR2t-y5ZQbN=a==n zPZJ(sh~tw47Z?0x6db>xj~I_qee4U41~Cw)2INxFZ#6<-5sgJd4UE0(pXl!E>P}1q z$h;%UVc%{PqYEUX5L;{{x4s<5xLC=^g?HX)2Z{^W#~_C$Pk%7uYor(FDy8zoCV(Pm z6CFiZq1nZCMw4AB&`c9mB0Y`EwJkVr1ZN*l~X|W{gMxn z^1>~nC|y7%ZG0oSkxc%X^k`SsmpLwA5=8H1M~C|$A=Rt5$el@>&UV;vyt~^aW3zqG zQrWhR6FR#j=iD^w^B!E3DKK&r7i&M(u#$S829`@~A>E(cMMh=Y%QEVxblYYMi7B53?)nMCt2k{VyTSig302u{rkxYJqtBcI~^%bt8J_JF& z(=EP%J;h2N)!6tdanrJyKZpKKaDhZq6Jb0Ho*uUJ;0R^TZ24|3H4rcmE`Vpym)*E{ zRp(0ToKQ{zBK*vrkWuo>7j}32NMuJlD&jEoa6qjZG%i$?E_ug7ZDB+MQF*Qb0#ym( zklGwH-5Fv$U04XluqrFj_7{WSD zmf7w5?(>y9Ko}+-H@CEQ`dA&kBE9;DpE=c7~W2BnwYPZ$L%_w%pJ7+fMcUS;702i25G=z)v#|8kS_RZ83yJeqn(M4iP zj^A|SBIOwaQT_Z2Rm!Ced;yILDFzGJ^?s>kl(3!$zIkWU^)ArBlgvBK!q)a&b zjXv>r@1`$7>8-=Wx!(Z*K^OP^WE2u_+`c&MuqzZ#7>GOxrv_JaI|>YJ4!}lhm|eYn z{brBK|46DxmfJrrqIkyK-Hj@iCYX%EDk{146`_mCv9T+nz!<-kAF7bbwwsp5WfY$A=8UVw1hL2Jvyhpqfj$<%Rx{&Se zSDpWNF^8{*aUpimc9c=-exwB>3?;-PwZXXY5oTMciV~EHQht;7kx_GJsEjfwU2NS0 zTr6K78yi~?1;+BfbP!TQ!$lj^4&Z|CYfB@5fDM~f7`Q4YuxOd4RVwYo7Y6jIZ1TC^ zBV1hkQ&mTVardzgFcRnZ4OZ_cEy2afxGQat9!Qm>Ohl2=WP@$dI=(VW2tx_;;9xKd zBGM&$y3rTFbQGR3cU|nJbOAEzB*DeE*8msy7Z5IFAqAb

?FPZOhiUQQ<@d9~-xt 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�%;mmY;A6ZucMX!}IjJ|eWabMu4L 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 Eg09#+aR2}S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/splash.png b/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd2ec3bf1eea13b7d185bea3bc864bdd88881f8 GIT binary patch literal 25007 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

4P1<;|$&U7-EYs4Yy(lM` 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�+oaWQSq2 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

fvOYBY^%p3PiI^+4!$gl?arJ6oQfa?Vy0m*;!8w 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

>MhFMtDf7P&nxEQ5d6 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⪚PUnXl-qCb1Z}I^{S$`hxbtBgaR1* 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 z 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 + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_default.xml b/app/src/main/res/drawable/avatar_default.xml new file mode 100644 index 0000000..d2c9e4c --- /dev/null +++ b/app/src/main/res/drawable/avatar_default.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/background_dialog_activity.xml b/app/src/main/res/drawable/background_dialog_activity.xml new file mode 100644 index 0000000..80cff38 --- /dev/null +++ b/app/src/main/res/drawable/background_dialog_activity.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_splash.xml b/app/src/main/res/drawable/background_splash.xml new file mode 100644 index 0000000..d79dee5 --- /dev/null +++ b/app/src/main/res/drawable/background_splash.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_frame.xml b/app/src/main/res/drawable/card_frame.xml new file mode 100644 index 0000000..525731b --- /dev/null +++ b/app/src/main/res/drawable/card_frame.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_image_placeholder.xml b/app/src/main/res/drawable/card_image_placeholder.xml new file mode 100644 index 0000000..1ca515a --- /dev/null +++ b/app/src/main/res/drawable/card_image_placeholder.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_thread_line.xml b/app/src/main/res/drawable/conversation_thread_line.xml new file mode 100644 index 0000000..5a87f79 --- /dev/null +++ b/app/src/main/res/drawable/conversation_thread_line.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/description_bg_expanded.xml b/app/src/main/res/drawable/description_bg_expanded.xml new file mode 100644 index 0000000..db2eebd --- /dev/null +++ b/app/src/main/res/drawable/description_bg_expanded.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml new file mode 100644 index 0000000..2239a4f --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_account_settings.xml b/app/src/main/res/drawable/ic_account_settings.xml new file mode 100644 index 0000000..d13907d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_settings.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_a_photo_32dp.xml b/app/src/main/res/drawable/ic_add_a_photo_32dp.xml new file mode 100644 index 0000000..172c5ac --- /dev/null +++ b/app/src/main/res/drawable/ic_add_a_photo_32dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 0000000..4c894f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_attach_file_24dp.xml b/app/src/main/res/drawable/ic_attach_file_24dp.xml new file mode 100644 index 0000000..806cac0 --- /dev/null +++ b/app/src/main/res/drawable/ic_attach_file_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bbcode_24dp.xml b/app/src/main/res/drawable/ic_bbcode_24dp.xml new file mode 100644 index 0000000..d068c02 --- /dev/null +++ b/app/src/main/res/drawable/ic_bbcode_24dp.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_blobmoji.xml b/app/src/main/res/drawable/ic_blobmoji.xml new file mode 100644 index 0000000..be3332c --- /dev/null +++ b/app/src/main/res/drawable/ic_blobmoji.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_bookmark_24dp.xml b/app/src/main/res/drawable/ic_bookmark_24dp.xml new file mode 100644 index 0000000..803bca9 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bookmark_active_24dp.xml b/app/src/main/res/drawable/ic_bookmark_active_24dp.xml new file mode 100644 index 0000000..217b78b --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_active_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bot_24dp.xml b/app/src/main/res/drawable/ic_bot_24dp.xml new file mode 100644 index 0000000..26d4c9e --- /dev/null +++ b/app/src/main/res/drawable/ic_bot_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_briefcase.xml b/app/src/main/res/drawable/ic_briefcase.xml new file mode 100644 index 0000000..eeb8061 --- /dev/null +++ b/app/src/main/res/drawable/ic_briefcase.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bullhorn_24dp.xml b/app/src/main/res/drawable/ic_bullhorn_24dp.xml new file mode 100644 index 0000000..e290b24 --- /dev/null +++ b/app/src/main/res/drawable/ic_bullhorn_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cancel_24dp.xml b/app/src/main/res/drawable/ic_cancel_24dp.xml new file mode 100644 index 0000000..7d2b57e --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_24dp.xml b/app/src/main/res/drawable/ic_check_24dp.xml new file mode 100644 index 0000000..6541ee3 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_32dp.xml b/app/src/main/res/drawable/ic_check_32dp.xml new file mode 100644 index 0000000..9325c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_32dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml b/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml new file mode 100644 index 0000000..cb610e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..7ff119e --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear_24dp.xml b/app/src/main/res/drawable/ic_clear_24dp.xml new file mode 100644 index 0000000..0a244b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close_24dp.xml b/app/src/main/res/drawable/ic_close_24dp.xml new file mode 100644 index 0000000..081e405 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_create_24dp.xml b/app/src/main/res/drawable/ic_create_24dp.xml new file mode 100644 index 0000000..d74fe13 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cw_24dp.xml b/app/src/main/res/drawable/ic_cw_24dp.xml new file mode 100644 index 0000000..62713d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_cw_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_indicator_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml new file mode 100644 index 0000000..ab9d5f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml new file mode 100644 index 0000000..eb61122 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_email_24dp.xml b/app/src/main/res/drawable/ic_email_24dp.xml new file mode 100644 index 0000000..1bcee1b --- /dev/null +++ b/app/src/main/res/drawable/ic_email_24dp.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_emoji_24dp.xml b/app/src/main/res/drawable/ic_emoji_24dp.xml new file mode 100644 index 0000000..5a73c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_emoji_34dp.xml b/app/src/main/res/drawable/ic_emoji_34dp.xml new file mode 100644 index 0000000..b00cb96 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_34dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exit_to_app_24px.xml b/app/src/main/res/drawable/ic_exit_to_app_24px.xml new file mode 100644 index 0000000..ce5bd59 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_to_app_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_eye_24dp.xml b/app/src/main/res/drawable/ic_eye_24dp.xml new file mode 100644 index 0000000..83a3463 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favourite_24dp.xml b/app/src/main/res/drawable/ic_favourite_24dp.xml new file mode 100644 index 0000000..5826bf5 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favourite_active_24dp.xml b/app/src/main/res/drawable/ic_favourite_active_24dp.xml new file mode 100644 index 0000000..2eb3014 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_active_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_download_black_24dp.xml b/app/src/main/res/drawable/ic_file_download_black_24dp.xml new file mode 100644 index 0000000..f5f7221 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_forum_24px.xml b/app/src/main/res/drawable/ic_forum_24px.xml new file mode 100644 index 0000000..b9d066d --- /dev/null +++ b/app/src/main/res/drawable/ic_forum_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_hashtag.xml b/app/src/main/res/drawable/ic_hashtag.xml new file mode 100644 index 0000000..c7a3bc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_hashtag.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hide_media_24dp.xml b/app/src/main/res/drawable/ic_hide_media_24dp.xml new file mode 100644 index 0000000..106a53d --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_media_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_24dp.xml b/app/src/main/res/drawable/ic_home_24dp.xml new file mode 100644 index 0000000..4c6bc0e --- /dev/null +++ b/app/src/main/res/drawable/ic_home_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_html_24dp.xml b/app/src/main/res/drawable/ic_html_24dp.xml new file mode 100644 index 0000000..3896956 --- /dev/null +++ b/app/src/main/res/drawable/ic_html_24dp.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml new file mode 100644 index 0000000..4c2fb88 --- /dev/null +++ b/app/src/main/res/drawable/ic_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_local_24dp.xml b/app/src/main/res/drawable/ic_local_24dp.xml new file mode 100644 index 0000000..2953007 --- /dev/null +++ b/app/src/main/res/drawable/ic_local_24dp.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lock_open_24dp.xml b/app/src/main/res/drawable/ic_lock_open_24dp.xml new file mode 100644 index 0000000..1e9d0db --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_outline_24dp.xml b/app/src/main/res/drawable/ic_lock_outline_24dp.xml new file mode 100644 index 0000000..a8e4201 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_outline_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..717009a --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_markdown.xml b/app/src/main/res/drawable/ic_markdown.xml new file mode 100644 index 0000000..dd6aab4 --- /dev/null +++ b/app/src/main/res/drawable/ic_markdown.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_menu_share_24dp.xml b/app/src/main/res/drawable/ic_menu_share_24dp.xml new file mode 100644 index 0000000..dd1be97 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_share_24dp.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/ic_more_horiz_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_24dp.xml new file mode 100644 index 0000000..c774133 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_music_box_24dp.xml b/app/src/main/res/drawable/ic_music_box_24dp.xml new file mode 100644 index 0000000..c0243d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_music_box_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_music_box_preview_24dp.xml b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml new file mode 100644 index 0000000..6790179 --- /dev/null +++ b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mute_24dp.xml b/app/src/main/res/drawable/ic_mute_24dp.xml new file mode 100644 index 0000000..bcdbb5a --- /dev/null +++ b/app/src/main/res/drawable/ic_mute_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notebook.xml b/app/src/main/res/drawable/ic_notebook.xml new file mode 100644 index 0000000..93ff789 --- /dev/null +++ b/app/src/main/res/drawable/ic_notebook.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_24dp.xml b/app/src/main/res/drawable/ic_notifications_24dp.xml new file mode 100644 index 0000000..d2f7aac --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_active_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_24dp.xml new file mode 100644 index 0000000..9a60daa --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_24dp.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_off_24dp.xml b/app/src/main/res/drawable/ic_notifications_off_24dp.xml new file mode 100644 index 0000000..627eafd --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_off_24dp.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notoemoji.xml b/app/src/main/res/drawable/ic_notoemoji.xml new file mode 100644 index 0000000..55628c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notoemoji.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_person_add_24dp.xml b/app/src/main/res/drawable/ic_person_add_24dp.xml new file mode 100644 index 0000000..dc849f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_add_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_photo_24dp.xml b/app/src/main/res/drawable/ic_photo_24dp.xml new file mode 100644 index 0000000..d0ebff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_indicator.xml b/app/src/main/res/drawable/ic_play_indicator.xml new file mode 100644 index 0000000..4cdae50 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_indicator.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_plus_24dp.xml b/app/src/main/res/drawable/ic_plus_24dp.xml new file mode 100644 index 0000000..2ba0da8 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_poll_24dp.xml b/app/src/main/res/drawable/ic_poll_24dp.xml new file mode 100644 index 0000000..5e55dc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_poll_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_preview_24dp.xml b/app/src/main/res/drawable/ic_preview_24dp.xml new file mode 100644 index 0000000..10523f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_preview_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_public_24dp.xml b/app/src/main/res/drawable/ic_public_24dp.xml new file mode 100644 index 0000000..6ef182e --- /dev/null +++ b/app/src/main/res/drawable/ic_public_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml b/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml new file mode 100644 index 0000000..160b237 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_18dp.xml b/app/src/main/res/drawable/ic_reblog_18dp.xml new file mode 100644 index 0000000..029e711 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_18dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_24dp.xml b/app/src/main/res/drawable/ic_reblog_24dp.xml new file mode 100644 index 0000000..0fe908e --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_active_24dp.xml b/app/src/main/res/drawable/ic_reblog_active_24dp.xml new file mode 100644 index 0000000..8d28a40 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_active_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_direct_24dp.xml b/app/src/main/res/drawable/ic_reblog_direct_24dp.xml new file mode 100644 index 0000000..0f53287 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_direct_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reblog_private_24dp.xml b/app/src/main/res/drawable/ic_reblog_private_24dp.xml new file mode 100644 index 0000000..078eaf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_private_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml b/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml new file mode 100644 index 0000000..43169d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reject_24dp.xml b/app/src/main/res/drawable/ic_reject_24dp.xml new file mode 100644 index 0000000..d11cc5c --- /dev/null +++ b/app/src/main/res/drawable/ic_reject_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat_24dp.xml b/app/src/main/res/drawable/ic_repeat_24dp.xml new file mode 100644 index 0000000..aaa76ae --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_18dp.xml b/app/src/main/res/drawable/ic_reply_18dp.xml new file mode 100644 index 0000000..234bc07 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_18dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_24dp.xml b/app/src/main/res/drawable/ic_reply_24dp.xml new file mode 100644 index 0000000..6085ff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_all_24dp.xml b/app/src/main/res/drawable/ic_reply_all_24dp.xml new file mode 100644 index 0000000..9da31f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_all_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_send_24dp.xml b/app/src/main/res/drawable/ic_send_24dp.xml new file mode 100644 index 0000000..8916aa9 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..b852054 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star_24dp.xml b/app/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000..8689142 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sticker.xml b/app/src/main/res/drawable/ic_sticker.xml new file mode 100644 index 0000000..671937c --- /dev/null +++ b/app/src/main/res/drawable/ic_sticker.xml @@ -0,0 +1,16 @@ + + + diff --git a/app/src/main/res/drawable/ic_tabs.xml b/app/src/main/res/drawable/ic_tabs.xml new file mode 100644 index 0000000..3de93e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_tabs.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tusky.xml b/app/src/main/res/drawable/ic_tusky.xml new file mode 100644 index 0000000..0dc845c --- /dev/null +++ b/app/src/main/res/drawable/ic_tusky.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_twemoji.xml b/app/src/main/res/drawable/ic_twemoji.xml new file mode 100644 index 0000000..70c4b51 --- /dev/null +++ b/app/src/main/res/drawable/ic_twemoji.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_unmute_24dp.xml b/app/src/main/res/drawable/ic_unmute_24dp.xml new file mode 100644 index 0000000..fe37f7f --- /dev/null +++ b/app/src/main/res/drawable/ic_unmute_24dp.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_videocam_24dp.xml b/app/src/main/res/drawable/ic_videocam_24dp.xml new file mode 100644 index 0000000..1614d02 --- /dev/null +++ b/app/src/main/res/drawable/ic_videocam_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/materialdrawer_shape_large.xml b/app/src/main/res/drawable/materialdrawer_shape_large.xml new file mode 100644 index 0000000..ba626b6 --- /dev/null +++ b/app/src/main/res/drawable/materialdrawer_shape_large.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/materialdrawer_shape_small.xml b/app/src/main/res/drawable/materialdrawer_shape_small.xml new file mode 100644 index 0000000..7bdd429 --- /dev/null +++ b/app/src/main/res/drawable/materialdrawer_shape_small.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/md_bold.xml b/app/src/main/res/drawable/md_bold.xml new file mode 100644 index 0000000..99b958c --- /dev/null +++ b/app/src/main/res/drawable/md_bold.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/md_code.xml b/app/src/main/res/drawable/md_code.xml new file mode 100644 index 0000000..aeac52a --- /dev/null +++ b/app/src/main/res/drawable/md_code.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/md_italic.xml b/app/src/main/res/drawable/md_italic.xml new file mode 100644 index 0000000..274ead6 --- /dev/null +++ b/app/src/main/res/drawable/md_italic.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/md_link.xml b/app/src/main/res/drawable/md_link.xml new file mode 100644 index 0000000..1220397 --- /dev/null +++ b/app/src/main/res/drawable/md_link.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/md_strikethrough.xml b/app/src/main/res/drawable/md_strikethrough.xml new file mode 100644 index 0000000..1002955 --- /dev/null +++ b/app/src/main/res/drawable/md_strikethrough.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/media_preview_outline.xml b/app/src/main/res/drawable/media_preview_outline.xml new file mode 100644 index 0000000..a15ba5c --- /dev/null +++ b/app/src/main/res/drawable/media_preview_outline.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/media_warning_bg.xml b/app/src/main/res/drawable/media_warning_bg.xml new file mode 100644 index 0000000..93ff4e0 --- /dev/null +++ b/app/src/main/res/drawable/media_warning_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_background.xml b/app/src/main/res/drawable/message_background.xml new file mode 100644 index 0000000..36c6606 --- /dev/null +++ b/app/src/main/res/drawable/message_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/poll_option_background.xml b/app/src/main/res/drawable/poll_option_background.xml new file mode 100644 index 0000000..90aa51d --- /dev/null +++ b/app/src/main/res/drawable/poll_option_background.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/poll_option_shape.xml b/app/src/main/res/drawable/poll_option_shape.xml new file mode 100644 index 0000000..da097f3 --- /dev/null +++ b/app/src/main/res/drawable/poll_option_shape.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_badge_background.xml b/app/src/main/res/drawable/profile_badge_background.xml new file mode 100644 index 0000000..be4bcf3 --- /dev/null +++ b/app/src/main/res/drawable/profile_badge_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/report_success_background.xml b/app/src/main/res/drawable/report_success_background.xml new file mode 100644 index 0000000..147e048 --- /dev/null +++ b/app/src/main/res/drawable/report_success_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_button.xml b/app/src/main/res/drawable/round_button.xml new file mode 100644 index 0000000..a6c0da1 --- /dev/null +++ b/app/src/main/res/drawable/round_button.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/spellcheck.xml b/app/src/main/res/drawable/spellcheck.xml new file mode 100644 index 0000000..79f2251 --- /dev/null +++ b/app/src/main/res/drawable/spellcheck.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/status_divider.xml b/app/src/main/res/drawable/status_divider.xml new file mode 100644 index 0000000..37fbbab --- /dev/null +++ b/app/src/main/res/drawable/status_divider.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/unread_shape.xml b/app/src/main/res/drawable/unread_shape.xml new file mode 100644 index 0000000..b85c7f4 --- /dev/null +++ b/app/src/main/res/drawable/unread_shape.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_report_done.xml b/app/src/main/res/layout-land/fragment_report_done.xml new file mode 100644 index 0000000..d9900a2 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_report_done.xml @@ -0,0 +1,109 @@ + + + + + + + + + +

R#uH8|a-2}QBWo}l}l8~V`}#2i1A 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 FV1nzq^#%X{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/elephant_friend_empty.png b/app/src/main/res/drawable-xhdpi/elephant_friend_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..ebed0371f523bfd08277ad5f262222814253367f GIT binary patch literal 62276 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