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@ 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�{~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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 z

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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 zcmW++WmJ?66Mbmu2C1dHq-%kd?(R-Ox>5`6>lJ0I;T0laS5QOi%-;en_ zb7syxckbLX@md;+xLA}}007`BE6M2q0Fe4$yg~;6;8YKAR~mk=-L(>PH!D=-_$ZjmKdFpmd5KzYEX&I;q)tk^N5SL zrh3OzxCY@O|MTf1=h@Lqb+cQ)A!+Lk9}Lc(FLY^Wzl7V6t-n&tSyjDbsv{L-ALR3Y z#R=U!I?K}TAS=npT;6efQn8mCW9c0$2Mz_>(z;ez(_d9}rF!p@@tkOJXJJa30LL*d zNOujCNud1++wkpoEep>n)$gRf4iipCz9fdrOWjzUi;xaOx{}W;B;1ycHb`_*jMA1m z<|6K{>Vtd7Dm7;>YsN+o65j9ncLd4EtJRDv-`JI?IVk2%C+pND8Q3DpWf|FDr|a3u zc20+@$fBQet%dn_B!pQSNj(;GI*~t>^n4DLRZxJVf)OcE@lRr(4N0=yp$Y99KV2L` zb3a$SyBHtuik9Z8sd||=)(SUq79RhDmw850-gnR82#r9E%T#)Qn=W$+$**~hJ>d{$ ztS5FH{q)PYrf@nqY23{L1!)l{$L~`d4-xge>z

L=2#RKlc&(*b;6I*z1i#Oy`> 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 literal 0 HcmV?d00001 diff --git a/app/src/blue/res/mipmap-xxhdpi/ic_shortcut_compose.png b/app/src/blue/res/mipmap-xxhdpi/ic_shortcut_compose.png new file mode 100644 index 0000000000000000000000000000000000000000..2fccda64c552c5e1d36b632785c1a92b63f39332 GIT binary patch literal 8414 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 JtF#=W{vX%RmxurW 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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 literal 0 HcmV?d00001 diff --git a/app/src/green/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/green/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..cace1c4 --- /dev/null +++ b/app/src/green/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + 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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 V9*#C&CjbDzfA18eDkTg9{trLVNumG% 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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 + + توسكي %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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 Sm16(^0NR=c8kK4e5&r{C$R!j2 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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 Q0002qDG00009a7bBm001r{ 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

l2p9LFxB5aHn|7KOh(0S_LOx8_bM z68Ht5@LJkaSo|gh1E|5J;C-dC=zjVxFt)5MH+3tx@V3d+5;y=KQQg-Q$0@}HBT0j) zl;+fEvmoyR&HM<(;=$05;K7+cet(Ty;LH`VP_t&$CkQoW12fmdo4?4fqoa%OAx4pm zivv4VsD`qH2C)>#jGNb$CI2Y;+P9uKaLVV39o19cb|R_`eJm2=;53?nQJ*;!(AZb) zlSM2@dM3|gSU+GamH>k#Pq8d5o~FNp7%jihGjtYSTJ0Esp-P~0`z-j01Ts71Bl&aJ zS}JyWV>>e1!dMgr9^&}>&LcQ?W&Zq27ln2FqG+fd?3PudPc5_8ST-(k3nH?^^S+5u zq*jXBDZXXx#;VDnD=~r2RoC)8j)LY=CSX7U{@Se!JvLH;(TOU zOAr!Ed35LB1PrB-f;7Q~887e}htUjMj4HlxnSrr@wYIjS*vq5BI?l35`pZ>JwSwcN^bX3!?lZA>r@9OwR^ z^dyy08hSI8i0~Q83_3Pa?Y0Jsja4#qoL50;+~NW%`_cshKFB)lnmoF`&!I@{t^LR4 z#m-J8emu2Yw?gj@cB;G)&t+^o&M49~)0Q?P>jBw#-H1^L`c1nz=&UabSNU&%$PU1&W!kgRg!_jTD%P2SlBMChig2avk2AnN9 z2a4^kZk8fUlsNtgp&uS)*{9IHSocW*JK1RL_R{j;mXj#1*VqSnLRs<0x(mi!To>M~ zWrg?t^QB4aVNPW@&fV*hj=?d&&|cm^P^hCC0KQQzd2?NbMdza39gz*ge8wB9-cGO@ zJB>=EGS^kbQ(L0cXso*ue%-^OQt8EY7?$wXaH0Vx-qek7{L1P5Z#(n;{~=-H-{Ai7=#WXLZD^ZC^92CRMkLH>cA*V^UC@7mTSlU8x4oS|T;NyFmo7^IwX^)V>`9i=qtViPmRu;pW0`a@Dz_#Ry5E z-he3SymEIrS}^< zL0nA0z;Dov#S76$!Py4bsX{e|mAV>5q00aPDHM)Da2W_iwEYqjA}I|m+>y&#_T6{} zTibGDDtkWwqC)mkG*qx>u*7G$yUWF5^!HC<`0lJF8Yw7y2Rrq#T{4&7VlD*nhuTQ^ zTJ+xtX@;2)r*WLpv4Qm#9xWm);UI)m+1l2!s}@`&8svkZ(G~WLEUOk)e&>W5*g`Q_ zOc_SEO^QWR7DyLAV=zn_?NG&caOK<&St+(m4Y8m2p(f$F1f+B%#A!gmK`Igq7=P_> zBbPOd8+;1eTXf0Mt28P;G3a)_Ip=+MR*6Qp0w_>R=l!-KAd~2etR-<6G(GYY2IK13 zj2hMLD`yNT!9Yl$gvu;wPBaiH)***|QPrEBm5f6*x5@NaiiTC&qK2x%Eq*)6o>3!J z6wyfM_A89Tp;%x$XP~=_mI4b%zF3b(KN4yf`S4D`i=~oD@4{FxH&i@X)q{Z)Ek6<1 zsfY`fBsD@T6rn}ZtcNr~13ORH+wL5Lgu9_9wzp><7PaP<%&c^YfeRcCtjc~mI*d|c zL0r_+p?l;FVt5`sHjbZZfBsV_UIfKiIIWGOJW`rH^+uzNSJ4Im$9u{#%?%( z0{;t4P1AQ5#riT)epUz#pa#b&Si2|bNOwW7s7t?#xoC}}F;i+xZmc*1_Y|29NeYB% zkPy`#J)GLRpV1XI({8s89SR>qP9k(;{E@rOt@>8aXN6AtGXwVx9Ct{Y{_xm$%%4v_fpuZwc=yZe`CmUXv|yZWosCBA6ORxe-}ign z^Yld8z(lEd0Rq!{jIGXJfg0s#YG5ms8M~2?SR-i_XCE!M{U0aMZAUiixK!AVgbf5 zI}YV64_?01w-X5priQsXDBb*ZK6w~(%Mv4!Y+)+3v3b1eA~Dbp6x4ByKk?A`=lrYr z`B!Akzan4bkK6O}|M;ETcn^(Qy~(uR?49gm9DouAqPs={xe&ehvlFmV*nAKFWFo_% z;Q8TTaC*VC3sFpO?H>#@NXB|krZ^Fyc1- z;??VS|8MT3Kxbn2TG$# z$mk%dTwImHCS4$nD;FUF7phd~?_}PY{g~aInVntPUJsnF@!Hlr`@EmeJF|F_r3Vy_ z`AD3F`P4J%w~ppBhdpkKk4Jt11*^d~m-+CdVLlvK+6#`8^1>+H$sk~MRHvdQ(!UIj z;JuPS-(G+D)}3y1S31A%b@zakQUEKcK%;@pK|cYmvBZMsS=Zma(L z>lcp{3<&N)XvWct=WBxuaE#`YvBQwCSv;+jP}zfe)teT=h)RDe^37#H3<&D9#Cm~+ zxkedp`N31@udgoyBzBGt$`DCAv;7Jzq_i;d{edv;ckPWeL^;9-B z9Dgqt|C|m_BWooc^GgHANI=3KPe%GGiS0D~y)0r?MAKzL?|oDdf~4?i+ma+B;xbZS zC(3D%w8r@v~b!4x(e z4i@N2wsX#jr?~&q>3lw)OlGsOhXebLf!1xSu%P>Tu)erh z%TvFrZ*IEhU-=FCQ?((~L(1k^z$)zTA8;5dFA*m&aobp+MtCiizF_S4oe&Xc%$GwK zV?xjfaSS4_d7KyYqcgX0ekVUea%T^$P(bT3L~DZpU;`&l|D>XLWAAeZ3Bv4#j2h~S zIJ#zk%k9t-fEZ5)$MHWe`2=-fQ|p>sFFgl^1DlW4Z6Bbgeeg;-UWV8P$S@&_zZ-9&BF`mhAiMujl$Ez@&m zlCMSKATNyu%QZMPWOnMzAg!CC-}csgj^5l{&N%bZ&G#-P~TzNVmlwv}t z>5RLYMin&mIRJ)6|L*GQ%N>Q{;^ey`H#ejrfDs6xLIX>dvL>!{J8B`nS5rrYCvU*A#K6FIW!3VWWvUp z#tLc-6Sq~GEha@_6KdRy221O2ziBAHA6j;QbSP_Nj6hwendQU|6z%- zr}5i7Gig@pl?tXw#}T_(#79!>)g66(Et zNM^tscP-W5$o4I^cc1`6jBzyxzsZaPu&?2shqk(1EQZ73VsZPhS}o5$C4(VLPvo(! z4~2o_mcGYC+B34miOOhA1TuQ;+&IHqevBGDJX(aUGk(Nra?U3!qsU@g8bY~Hw3`{E zcdk((5-VkiI>O*wLfB-}E0z4n(R3uIgqW?cX&WkL0|F?hg(6l`8z{yJOaP_a@WFuS zq!Ji}vg(AhLUN;qA5f)VL2NtpJk2VqfDWK=JPVb=dHULm=rh9UoH6k^Y*_aom7K^y zKS8UWnHoZYakxMM^x3Dds|lb8EL2Km3)Tuq+`b6HP7q5{Bxa=`rI})2kesN2rSy!{ z;2wO)_0MsKKlR_MWYMix%> zdS{A~`P7r1qq6h3RZjX`F#%V0Hs1h`Q3wgU-K`Zh#wO|`z)CgL;-4ZM9WD;fjAax6 zx?cQs$-$B;`hTg~Y1^CN!GxW5fJD8~tbxoHNB1*zRug{7>1f<1VpFPCi4$JQ!iDFwxg;5U1 zhV#hGb>syH%RT64qy#NdiZnYEl6u{nhhal9PQBiygs8d+Hs7S=>UeTaEYp#LCK*)U zhw)s(&Y(V$4;;i*mNG9%Dn$xOwbg1iyY6Z>TdiuXEa>x%MsO1^Ym=QgAy6Y-%}+%b a+4>*syXcn`&=$G?0000@wI*IvV7^VdHCVfbDh#eQf1(!YvihP=dE^dODmRlTB(0o?X`gKxP)y;D0WaV z^Sz5=I5Y9Ohxo*l_P~$bqH4;5W6y|W*Oh0hgI?2)X0U}_b)VI=iD1*5XEQZ7`Qp|% zLQ?V7$MM*yxr<@**~>IOOe!)xkz!lTnrXg|V<#>+_TABZRW$v}o&V!+=DetsW?zc5 z=wU}N`p1{rnQ1RLMS@v4?#;WIY-GrmXG2U{*`R3m-qP&JwKqFK!;@w3(!uJ)v2aWb zoqAjK+s@Xaas0`eeNacEe_o$+YG5`o-JWWPUOa=X;pED@=)kOfrrLm22man$zIR;R zr*(Ky39W&4} z>dK^Fc$acWK-{sNYl*N~Zi%RBPM~H=wuDiBR~K$pJ$-9gwUKMSo|Cq6SCC*lfmcs+ zTv^PNUra_Y;kKdT%ADQ9uH>(S-n5{`rkhb-aBGjfcw9ndSXrxkPr90QwUdgkZcHq)$D0EUzQvm8d%Nh1B2oNK-VNKgSc%F@M@lf)GqU2qRZr`NN;l(*P2x_1lL=w>hmzWq*T(lcq%n_t8vzXCl z#u-NKy=lm-+$iWqFG6r3G(-Z&O*zdj$3-w9=+6DUlPr!fn{Gt7i*zSkxX9uB1^0R0 zcg9~y+BCIoemsFT)ilg}{C=Nb?>u?(<G2zdxuR6U8k48QolTK~W1L34bXr=-=0j6FH zDN#uCk3>GPn4IhM+FB~``2c(=t=;eB9s{I#FUkk-)cU$nzD_w;VhU)6-pY}rwCbFD zpp|{jy}E?vDh=A@K=9^thEggUy_$@@fX8WjczhOIE9fo;%w?p$7|$>?A|#pdxNQ&6 zUBIW*hum{(JVZ%lqmM#*P>dGbtE9BcHIUEO;28<&K`|Qf(Y3$~`0&ACI9%3i3}LkJ zaQJj|jh}^n5Rf25qbbU0)$6$5`Nu{1Fde--6Fz|e0*Z1hE3$jN68!F^mAMDNr>2*K zv*800LO`M@sj4bcM)%fg4(~HEcE)l&ZB~4c5CW1^MNyJTMa=Yb_tk24?+RJ<#N?Bm zU@#DvAt82Sy&( zz1kqnIhW#bvj3{@MT2y#npK1aA|c>|fr!*-8p@h{e+Cgd@F^7Hg%~(&;{!KChtXOF zgcPEnYPCuUNl?~uvHKz;2l%T15eGn}9fjiqvnCYgBq|{!RHTGBq0(VuOZPxV^WJAo zz{7yY80O@CVzz+D;xQPGiiA*=5&}e$$Uz4QGKzV;cUwjau2qyzZcxG`^!&dGZl>yi zZW<#jm?9byQh|^l2*zEPkZvu#PjFOTVPt}xAxZKOd;B1){(|YiMKIHQ%x#X`$l)6la zBBG-QR5Fnr5`r-5kP3aayT>PT-pB0*Do7rD2!)bF_+O8xtd++I0UQisG?4_$a=>Q< zD9GP289{qU_^iLoK;O)32&gB8LJ9K8emx>y)7vR)?Lh#A@rx#-Ast~15PVGS8p`NS zEJYk%-x}dVr*~fKARsjqswyg~KPudo5Iul}J5K3?Hb2@e3gICSfJ9p!eourqTK4yvk;t0+2OZDaEj*FtNqe+C?8V|tU(JKZ5_ES1h#(cw`xL_b z4Q2{zN)r&SS|OtSQa=?Wgoc?YJX8#QIXI!x6p2b5WaNKp{{iN*@D#dzBQVqk-3MB#63jU*{N_+KOFA%ie4-ZviVd1A9j39jQ5u1&3 zkkNM@LmYE16HlaBPmeF3gV+dRqi6vlW4FYL<}N3ph{9*jFsyWfN&&RA;Zc-KEvFMp zO~y6Z%>o37DkmVL@0YRd3s2qtH4{&*XH1^Fms{~zE?WdbMQgiN#Hm|N7C}}aw4VB@ zN#7I>5eOzOa~Yk)b+nqzA)+Zfp8!PpKxDSMaJ9bgVYPV_Pr<}f>Q|=EE_fZ1P%jC9 zs^-LG5=vGnAdwnKsK?G1{CwtQT=2o*K9ZNi* zo__Z7Q$J+bz*!>?oe<7T)MctD6)ix3sG4Lyb;?7$C18UCfIvdZa9yv6TQaEVs;qpC zgftV-&Q4|P0;b4EQ5q9m2CPsTFrm!bYyq0 zE&F{=xA!O8!o0&yVA<{Sz{ctG58;4#y7iao%FfPJgs_gu8wmiE5Cn1(0e6;Ep@b>< z&;%l7y32-U0LGIbIVq%o1n3O_5M6L2==`oqTu+vJpMNZ3OhOqdp-MKJ-a3J*R`A_+ z0qJm>3RzoT_WOM#{QkAVkWHjeXm(GYZ>6)@%1#Cer6viHajZi;#sz|$S%TCLA4E|J zO+>yZ(I(isRmxM5gaGZJQ9bWP1jL&A4N3^Q1znc=W&Xwz%4if2XehnCy-{x#B5S_I z+c8@31BA#Gfh*PKdH14zvb(XlwY{B2LfU0Q&@0B|G}N)Y1a}~?^vF2+=bs##oFY_B zu+}hz`9zSC39@faa&*o@dqwwPWXJ6WNkrJ3BOxxV=a`np--M7UA%g%yLR*_#`-G8m z?pBO!%iy8zdDDt>h;FxgadB~S^lEo^e}8jxbL)CSypfADL_$gI3VE`sezW5HJs9a&f=Ho&$QCi!E-|$>e=|ZEjbodWj!B!4i1$`o$mrPO16JA|k^Cfg~ozdVyq=B4Qy4 zWhzO9u!5-rvh@41Rt};qwH71y!D)N|IN(F-mkEvaLS~+}2<^ise$)jW-KO;0-K1QN z810&|ze^=Fm4+rRs%1(jIX-#)^Yx1tf0PWddWDE23z0PbZdI~G1d**I6ciaJihKHL zqj7k6+6M9P^nJTC^8mAD4AJ0zPWwQBU z4bafjMGDapF5yBvhmcRHJ$ax)(OUf8;kBEnaXj~k6?M5cx;lVt(Wh>*qT6*5}$yS~V5b5QdyF3$1X>-^&P-Te_BcjzS3MKTS#V|eYvdt1I1@wPb&L*U-w2R|VhQ1X_DJ}|KDJX))W)p>* zfDGs&8ov^>YE#jL)rIm}3b7;%jx=7W2n`t|hEll+s7VM3VoXJ$pt%_}ATzOoXr=LP zilZ(9U3NF;oaf${ujy2$50cXOnVTYVNB?@Na3wR}fGQ)#z zcyH8;bO_OmOF(T2*@%b`0NPamAt94V280eGGxFd|1tDEw40#6-Er8q8@HuwipEMyH z!UKHL63ULUK4@7}#_DJ7SwANFw90AW^!Bkw1qdk{IW5`vrs`=v)*z2rBcTY`F<7#T zmxQ<=0TkXPiV{Oa3lIfFZ&n#8Yv@u!1FRgnM3i2b*lNvXSAcZ8q}F_bQ5rL|M)idC z8pFhgda^vDj&f~=U$7s`lKNwm&<-9kjJ8hYCo??BN`+%b_Ip|?>q2XuMkFvKiD9BWCx_}vJpUNYYHk-PsY(nZwL^{k`U^X*S zoB@K|C`w*mK!98(Zm2L<#traHz$Zr4g$NDzDujB|QI@WSGuOh&0ln1nlG3}yw{Osh z=hXnTJ@y#Mjy_n6YtVh?NgM{=vfY-dS(Azt<8GhREt_&sY`ETsC`JtB_6AkwJBkW7Z4Cz8y0EC>lMA1w~a1S z*~^y?{XMb|DTs2=WIzHb$$}`S2}fC3Njc6pO7lGGCN%m7({C5I_ruGH#JfazJA+cK zXEz&AXG-7JJX&qcPPnfr$TsjOwMB1;wC1=CjnQPW)Qx7=lJz(i(J2>Rb&#h(LKY?= z4J;Mb)(~bP1)1hpB-5vZjpMj2)ni7<^_- zZ_zU1K9!vVM(0F2XaT)~_^Y7+$(qj;0hohr88(|b=nhgV|cmGj?BI;~3j$Ve6G5f#Rqa7W_y2fk?K`xV6>Aaz6NF4#Eh8ofdLHC+| z0-uEGjt^DN4w*d6Yk@}3u+o(c4?jeEFtdd0cgyYOnZ|@kzX=`hNomXvR z9UV5x`5sUYVW;782@q$RCqzgDrv^eAnb4S*5c1sVZ2jKe3fLv(ZJewB+>2E%Kx&Rb zdiKagFxm@6RgkvTl_Ae$cJfD2{M-2>Inu1Qevr`t_}ytF|AH81vQ$T|3$aXgY~Z+L zO;uIRnngc?@u_Y=4h`yW9f!&N20qql%m_Oy%yhc~mW+Z*)78zQPQ;1KyD(FGg*k!Th@S3Rn;`drM~YE59A-w^D#m$;dIB@{aUT=!$wy0 zc69+E$asCw#zN~MMWO~n4-#63mH4o_wwR=z3rJLW{69B)AR;EWwzhgr({8ulh;_a024R$tu+C$DTOoS%J$E{Y!cp?$r|JTo0y%i-$=O%DW(}m|Va%%X|esC?jyA+Jo)6L0bKDVH9tL z=Wut7jfS|klu=t*xLnkGRv95kXPhB=?Zd){hzNoUslWqj$rqqUe1I0j#YffJ#>SN1 zes=z+EFv2)ayZ^W&6`+W4)1S&OkMzig{Jwnd0dMti^PiLM2sG~Nj~MJSHA6d=PriY zzKtOov{@6Y^GChCzaXM9z|fnU>e=7}f{dH4QdZKy+54^;XSyg70<#uG=0QM(2Kzpg zRUiNrlgm4%Zfu$L=>2L$To%hWgXdU*7f!C^F2t;XhTD}cQ%^kQ5nF6t7ghvg_UF>ox1+vWWWoQAY`REi0%9)Y8(~+5F1N3NkW1^pmTX zTysAbx{zXE6#NRnJv zz)J~>Sh&EEL|$2djTZ%xClR%;jBvdqqNfazjSyWzN+^Q*ix@4Pfs&54Cgt!d{x27< zi}2Kauxa0lfD{q!)uU0*>I&Hq2HD%1H*abt`mJb2A0|02eyunQNElkY6yMl6w!}9D zSHLTHd0{gYj|&263(Z@KfQVX*!v@33`uY0A1jL!u_Gu0XF-DGrBOB6(<@c*!R?k+F zl-$e9?%nvWr_>muh4}>jYOE9M0H~qi6sqNhf;Myv>{fxybL;;Hf1`{KW_@^#7~93L z^77J#3QC|&yK|Ffio87iaBc_^PV)mUpH7Fj&YJa>vvt{uNJU`S43W&~okHoa4@--e zcz(!1M`C0?Nv@7llz|Ub;#P(>&xYh6BcJ|l@WHPnqN=wB&T{oQ!wHc$AW54vA97kk z8#KF5&xbUslr#g*Bno3Xbz@z7t6%Szt%&UU(;OnoghVm-=jyy%QI?bz-}rIoK#MT- z*TxnW|6oo%2D<1`7qTHIbgSgk)?XhyU?O_&hBzC#s#}kN2?+upXA?Bl=155LrU_}V zvhMb}(7Q$GYXFgP&#rG~l{MJ)Q|p}dlXVW>G9V?w3LTtG|LDJnS;THh9Zau)3;j*p z)xNZqW^pKGwuNOWLtENUyKR>#4IRHknHymkFv8Va5^v&7)LJT%tsl0SV3~w#C6)Rb zYZ68zb;Y7#o!VemOve|xL`V#@i3E3Jzho;Ezm$OULD>Ib&v~AEV{&uTMt1l10ZWn6 zHa~yocV3=z&ePN-g8b^$WGKaRkv&QwovxRV2`Ej*gTuJd^gS%8u57K0^97a%z@PIdeGx{^d=Epx? zyK?ceb}MGT!sh%IzC0l%G6cTpmnJN^txl zAVhSU9x6q%b0Wv3+fD${a&jKBcdVaD2qi{F5Fz5-3~e6oG6{#KM_v@xrZlv=*La?h z`oFQ%IM8(?BVPCd6N|sO0^v|uZ3sU|kfim*)yt=cO3??NV^-SQtA&Vtaga>T%p?op zj{)!OF&9h5(ek#JrjzLHW|HzHgC4REo9`~eh~cB$y-b%~PsWkE(#+S|BXs&Y!*8=H zwV{-g8=VDL@6Z(;2g%R2o}36G45=X1m|d z2Rbw+VQ6dbs0t$5+kpLMsZb5Mfu;;1Hyrp2)~T0^&>$odYS;K;iDdQO@E0(%Q{R-A z#fVUY3~klb4VO{~d_Hk#H(y6M38FytB*OlNrKM@oS~W95+Zbm^PZ~(li;`wf^!PTP z?+#xS?!)))Fjq96oIYRf>`A;Ba&il^r8+J7d>UIXh5@1#$kjWmfQaoP7h=;RBQhp4 zGuT>1S{dQ%U@51(3D(y)w+b#xr0B;72#q_n_hy%QUg_*fyqMx4<*4g(G%692(+>hs zIs_1TsuAlYQYV`(AhZ;Bau8S_V6X5Cl}kMI_!eJnR?L^6cEnxO8lkWGvdU_JHo)@F z`^t^%*yqnm5LFVR($CChmg(Izy*NuILNzOpF?VIw*E8x%Fflz7T)xLeDyi2S?=jc4 zHnWR7msg9ZQK1*WO2fsX zv+7Vj*;NCPL*jV!I}+VMT{@N1LHgRIH&S%A3rlrphF0HoINEwRGX{(bt;> z_)8iuN4>&uijbPLnL|V!+3i!6AzkCxtI4SUcnp;|UNx#d+qA#bzrL zo6L7OZ8MKE*BM3;QhVA-&e3wKZ+J*WsC#~S81+xQb@Y?`qi*+&$S`~`9x{|+lnGm- z;dPa(>ZEw%d0)fQ()4WX9kUU z-a5La;97#{=IU(`XyjZWB}2fdC|C1tPpF$3^_W4>YPZkUpsxrKkf_#Y`|~<(;K7|D z&8Doj>Fen-gaX9M5-rxr{-ItKe4cd|5|Ocf3eoJ=9BxkLMN$w0jM|Hc)Rm9=V~I@J zq6YfU@XTGoSD8d0&4fgz^)9-{sj0c4^{%FIHE>9A(;*=g1%w=ritG#UiM}kxhg_mf zQ5NmK9>o{9oFrJL7|BFpZ)8@YR*<6`Djok25{0uqpRM9L8oi`MPcza!EI=r$XdU?%TPD!Pc-)M-LpnTBHKGOC0W|mzi>%8hL1HeNF5T9Si6K#iLFsHOEbwl3n6aB4&-PtQT+d@knO&?O}ygE0V)EwxSXly-j2{As}Sav<3nQ zrR|jwYV7X)>FonzUX9)A35Wyqn));_HS(xbFdi!0YJhALCb4YSb}$+hbD082)XYTC zY9ElOV*4>k@m)eISuTe=)yx*#f`-W%Ahe=}P(12?=;-a)csseX^YHzv9yR(D>9y4A zxc59lFcJ$GNmK%qSiL=y1CS;PFEWufw=~1wg}B+)R8gvNB}6UuxHr=kpk|9Ou3>V9 z2r0(62}1i(|ASZWKK}ag-7CkV?oZ%D#AvvH(bgPbgfDj`QiX9c8KnR!QeKS4f;mzX zB4!4eNBGbM-s%$wnT1-Pfa~h$zB-FwiD(x!p1ss^bVH^1k&5|W{_@XfV~w9dN6JJ7 z3lP<9N*M9!VHefw!_$&}tS}sabq3!DeZ|jecf)f51J! zT-9nt<5w4y2uWSAfY6jfR|Pu9K25^t=H}dj$m1gx&O#|jL8br>AbP2pt$8d_Jqauu+KM6a@1E$VkVsoxCkpKW7 z07*naR6nFdiNFLnbx39=W?e2{YlTXh9udO)746K3D^@HP0Z4THv}Oq973Jj)@XR{= z-Q5C|K14+Nt}+?icjBAViTFM}@puBHnz>C^XR+lMN-UP|!GF2QmP7mG8SpatneS+&hMqIJPY6;!|n|1!%*nZKJL5>hVHyX>1|#Xk_#rG zNp5m;ZQPV8A`Ns(A{7$UWtA!ulbVQquqX>wCWEq`W32tbx!wyc-XmnK*Oc`wE~Q0_l~(|3?w^ zon@qNxaNtq)!F?Q({Enmv~q>#a7%(y3^U!oVG~GO%gOtVE!3^rYNHuUcuzZ}p`s>D z6i+>Lo1%$`5H-<0O=vxrn|V24xyF){VAh`10Akg>wYIQ2GdsDzkN4!Jr>CiX)c*eD z?Cgjn-H%hxnUhyQTdb5fvpL91q$})xLImHzlt~F|B*YCmoTBrzPH3zU7=PJgxz>tC zdIoD$wz2T{iLLFmwUxCMTx)A<+gn=`ztA>oYEr3lb8U?b{(>nY)I6QkI7{UP#58jf z)(tn^f{@E`k`O4Ot~>qFYZ2;;Mh5z;rvJ=3AzgQx|AW?yTRYimZ(19KJh-onZ3YX2 zGtkv$gdRX>$x4laTn={G3_|mn^vX32^?Cgd6$B=-{c^mo}$5kf^Y7_nRz5r|RG;Khh)g=p;!IE@;14B=)y)>FeQ zkdVgPEbbT0*TBtI=Oq);L=A5D62B~$hX(<+>V!xUby%*yqJ|hvT)frrpSS)YL2QQG z%;BWzQ!OVD;k7($o0?)m+*-_`27|k1Y_q=rA?|?d@H(v3?r^M#dY=tgET6|{@baz3 zf;1bGl;_mBh&%9EYc&z}K%hS9!fE6gIxbHz{!}r+n%!nXE+C`|Lg9OcA}Vg5F=n#d z-rg?D@+aj*WXq?iqty{b`Wpi9Q=i$Y5Veqz#q2#30#)thDLOWXmKutg5P95oOCR`E zIsEohgw_Lvh1wD;xn1syOpPwX8lCEhwzq!@BN@#a2-2m5)}|F{1nqs0#KQ$bR2`v1 z*?YgR_o<-Q&T~UtYP>WeeP;%tf7+>`iCy#9s?8cGG|L5Mjrth5Jvz0xanv=JF5oK8 z4R0(buVMsDlO1>^Z15$y*%uMo+6R083Pl7v7!o3@2svyn(FPn4F(H;Rh}MGYya^fo z+dk=M>JRL)YhGK+2|{BSyz0hmDt|Or$OST)415Rh?@Xa9f2q6F9v$5Qq{U)!ZtiZ^ z@c8V)_MH=$sz1(xy?;*qufR~zCSv!RhAZ1?5FD%&nR^D*YISJZp$U~Tw|&YZFZGSK z2URxrZ8{-P)lc|xncT6{RUk68E0f7>T#_E?TO5XGh$XH-AXg~%j?b)2oP{*8@LG}X zcZ#qAKQO0F_w;$f8AYsdB1DbVv)Qau)$W^+B)Z{KhP>2T+Y|J=P7y*?J%GAoncnN! zm6_|fP-Dya!dax5Kra1ceDw^ZwO1gk0)V6VAL_*AXX?jT=a9`;4-%;vXL_$(shyXKLyg0+tsax zJw=q?DDV=9><%&2OK;ChsuEmAAS8+*DavV7FLk9;(!*gjig@yn=nVB%MM_Uk@J*F0PG#duhHSa8A z5G7bJ7ub6s)QE(_ZEY2oiG)BE`BN#bQ85BDG-l~^M(B=W@4?RURMLVjHGH>QlB0{G zmuC7S2Zh4jqm9L>h;TN+pSoMf8I&@GUl)vx_Ck{Wo?=2c{Z^$I9{*%OG-lE1M?%h4 z1cZc~&z1UX>Ti7`9(+B16$q)en!^sd$eH0CQRv!RJm-Y^ssroMo%~d^`Vc-J8O)xHZKygfe3|R!8Th`>Tm78^=*1^+oM0$ z=|4?qocVa?@}?UpaPs4A(K-KF#_6Pk&Uxko~AO(0VJ zf)E*aSdKUl0!1ATvk;j>=p#CebePboxhNW{Iz;R67P^+dFdvIv*>-?#R;GZR=$e~X zyQOYH2+bKWVi_Vpx`cyguX1cDkdK>;l#SxtWGGLI8^F>WcKys!6V2Gqdi-DP5BRKLn3J zC`2+8UW(P<9csAge(JH~P*Lz0n>sa0a5`)AA9MX3eRU+19293B+ixiBNX3J3;WCo9 z=%6c%I-yr;P<0|8Msz6EzplCgQ;aUmGI;!P>|2fjkSkk5s5n|@Lc*)o3F(8HX(SPv zF#`}No{Fk@Doh9fEsZEFc|QP;5w7 zLw=svWM@FF0uabhsZ>q~Lg5EyB0 z$3$T5ai$6Z(ijlgc@`lw?I1SCLHyFdW<6_teQq=krQU3IF?0-bn|h!5(Yf94ka_LDuPRW2>vQMtXY?ab4sa_QguA2 z=3(=eIuepQ3_>H)>;I-0kjhSnenUn8LK!+f{!tJbi;bWZt%;{lIqDtuC6N(t*>r(9N(Fx5e(tSg+lA^3D8)# zNv)c0xVs-^NrKu!-Mg41^708orCK2~oV?l~jb1l)6CJUD(C?D+m<~C>lcu34TuiNt z5bv$f*!-BN<2UK5D;WC(i%@viH(xHPelJa2w>uvgF5}!)?_QqT>)lI!X3+8Mg{1uK z_oDpTUBrhLJBthm?}Dsz$HH|*GUui%8hf$H!kfO4a;c=MJXzeStLnFz!su1+a<7m( zx~9*>&7QFOV_NDS@_I^G`eD=g=-xCDqRqAWRy3XY!JIpaUw!Z16)& zWl5JSTy==@SBTJZF_+uB<}Mr1jXgMwoA3#h&<|>96xBm|xFG=Hti1Q!H~W@J*UA3` zrc8*?N1spmu?&b@Ryr?lt7aLmdiQ&|Twz20OhvsKlRlP9Wk(63c^c2xijJ%R6cT{O zHb=%-37K?r3L^X12PU)>^ZDF=9OI{0p;4@Q0t9ntmnRvcN4Z=&FMpmILR0pzTrOk1 zAdYVTA9LsT(pH+r@oEyI#%K&iZv`)oGiOOn#K5#xN9Ss>_b?#)~o+O)R4Sz&_7=a^g|r zkFgr(g_hO|o_zX!e!S0f-sk=EgcMMh0LU2Q;^Y9ju3%Fw&`}FU7GNw46VD)`-CcaY zx??|>g(uA?TiN{L{L}LhvWsr0H7^YhPM(X^R@MpdUXkt!%4h-A*62p|gLm`+=7VFF!5MBN}F(s(5& zQgM2N{ZbH#M%HrUK1ao|{4*r3qkxR7Ts&4-K#)-Xh7a-^0Alp@cqSMe5izswp^iJG zt(XwZEWCUvghV6z{M#hO`LIIPoq@2CA5e^Q$2qo;fUXtmxF?VZt%8YJ8cKkefngIw zL__2v1$5P^1c_$%-d;xEb>ORiWs7HnEVbH62<4WTDCT;bZq~^*)fN(vlKANtP85)~ zRH*7(;(~~9)(de&37oftB3d$Eo{_zJ$g^yU5bEjf+21qo)h_P75zIgJE zMFACyUt!!v5K(3ao^e8W;WHj12hA=+M5Fe*q|MQCKgm;0W2b+V8~|jD zQ{)$rqdL7pfHy38;@M62iVwz3GECC4GEVAaaeGG9e#51LJni_e&9FhqbP1u&|!3jaa-8fX%&fbolT+0{MnRB5p$@$kQ0MvvM zk|OF9HX6CnDRK4+?lCDnuEEIpz|MLy_zqLIaLpUXrcT;>WZyowliTU0?PDLE>Am7H zVOI}f42;Z42FjW`a|`$KzCBV#)zAKL;$fdQ7|GPUu-_SwQZwwt4LN}RVn`? z5GvCn4igzKh0&MPtvCilK8}u?HXrH&Ev<|v!*~t`;PA=$dH73v`y&5#x$oqXk{xWU zWV)Aq*{w5U?kZaaSNo3 z7AIfBym9K^$xOaS8x^XkfYA zFgY9$pGX28e{(33_BcMyEr)k6bkl{9DzBQ5ypaL7W0Lp-Jl(-ivFtEisfv16?k%=% zJnbXLJw#6pe;$~8J-8TtdwlF0gAG~BzsEs!A84A`m^7^O+q7#f;uYY)?~Z*;j&!iu zMHkgoUb!b?z;Oz*{!Lk#ciy-|PAIV{6xAU)^o_<`E={ z*Xy-SRA7HBnh*5jkEXMD93GF?>xEw)k0%Stk&gQ5qUw^X6_J5Kn?i>m0wUK6h=^LH zpL^iA=x*+v9&g{s?~BIlcUzvJg*BAa>sj~y^tk>l+7(R;C_G-P)!Np!Kvr9u6*viy zydgiWuDVixRIFz(1CsC&rsjh+m=IBN`E0L}Pp?dy^8@MJ zww4z0v@NU$qidgz%=!IuBeyrB!Syx~k~iw2%dgbb>UBggCZja1jjUgYm_5V0L1PV;C;x>rC*A;WyKhedpe6e15+l1ypjaMxO&@ zjDdlLJn-RBK9T60jkaxV{q2iCRp8^6RH~}WivZGQq|J-p^sy&&@iJ1DRaDi<)N1m7 zqL#_Z>#9_h$J`mKtOZ0Vw#F8v9uR$XA8Emzg46|~I-HjQh}@3PrY!m>J5WobSY z>6imSSnS3y3}Yb!1N)cG(Eu`kH2oSJ5TE>xtyR`GHLWIc0`i9(0-w;QHhPs%ZHzR$u?-3oWekTA|Uk8gwF^~Z?z^&#*~xN=cy=` zRRN>5h?Tgbh@URMn370kxx#i+Ov0qg;YmlLGmxpOt1AybO|gNvox_ZbiI|hoY2_az z`Khy4Lk7lRqXv9%8~XK2=U-m3O?cDLz6Wxke-`-k`{0KJShgX5!3-I>q6_5Hq-SW^ z1vn$6N}UZ7`4?oWYmljyR~)i7xrPF_bC{9I7oZwzP-NHn!R3K7GBS@%E^Q1VpYZGt zzdGG+agx5tH@{8a+(QAi1;M_y;=soj=P14Q*rrBVbrC%hw4%rMuF>j>_x3>53R~2Z zm5fJ7P>HEYH$Calh)q$hDm~zg(~Kky@1UBgKgS7l{6$_OpZ5w#Vm-V;zA?T-j4ypW{g zvXbTXNGN$Cw}DZ`L0c=)=)mhx5Wf|qAf7M!EETZw+0ENiGaB}kQ4711}>(b60to1#kjet<^O*(7y6 z7>aIguC1+YZbn1F;QIOkN#z|mMl-bH(5_HftkGjb)`F8<0hZ37mG75vKYI+H`_1GD zWkno&wfqp$L%wzD`7htjvN$_m)TGJB+lah9-USfQf51N{fKQX5{tR8iN*O&TxYnTS zeGnYd4 zm+l8&^L+oxai(TA+n3+;gM8-4A3ehfIhkI;m8zmbrBYQ@*OklELNb3l8d(r`JRaB;%8t4| zWvTo~7Ore?|LUB5>4f|*uc@JRt)O+y58r?ODm!cF2eW1 zxaJQjob2Clm5^rFNOQxgf+C>{jIK<5H~Ma)h(N(CLbpINjUDl6&+uhdjqMqBuX{BJ>0{$m|brG&x^v{&NhFYz7zj) z8s8M=pU|dm2olXQ%&6OK`TUa~f2t~Ll3H-7Dh{cYt%ztQhv>rywK7`}#r?f&-m30X zsZx`c$F#N}bUY)=;KN#!sus1nrCL`)RQzM`eb%V^>D{LgM#k3aUV1%M zhKIbiC7s~;(DdZ==zEZn5hBrk=KbG3|Kz71eX0^+5Sk>392ay+mArpLtK7d%5J94S zP(_nTy-+A-de<+BLS;)Ucl?dG$ zunh3i_~zW*i?c>#GP^%JF^^X-<`GZnjhFGMig$dzJC-|%GcRew`rmail2&Dz-Dzuf z;cs@14chHA{23e^bHYbl&83W#P^uRQqBUFd2e|7tW*Jpz*(R+)SQ^yOn`|uzwI4bj zo1KEKk)3JF%p@UXg&5+)tqZBHLR>I z;JJcd=^Ug@W>(V$OQX@StXVhcgbzzg+IeLov(=0CdSruXthO1+1eWLr#aG6wxE zgpq*g(8PF6xI5NhAH&lpZIV?aWbKIyZ_K6NTzNM81{Z6l$KYhO%SwyUMt?sBWY^`HQdY?%5r8|h5RoWq zHU~_;Lz&rz5|`VbVUREq5A_ru--^1eeB2%WuK0e_Rc~tE-5vewX~IWV0#g}XqFj`~ zm|hq@z=tgclxL(d+nrihNjtBWc!hDuVWe)gZXq%>^Uf=+(POm0{usub8>O$=m`X$^hjU zu`CVyC~4u zzj-;LN0VImn14_dN2i$pO%IY9<#Lg-3F$>suquU60g(=NA)a`sq|64%vzdro{UL-F z{E}LNub~Yhi0sT`V^|^64!0Par?5kzoJiW+g=E-s$H6kD9FEDp`a zp-POLV`GB^4@K!NAcTk@@WhCS6rq=^t!kGM>NUw*YBEpSv7GFfy~4zVIHSB?qZ%0~ zQgVq{P?4Sil4g$8n_PJDVnq6opz$)P)>+6X%(OK@j8L!7gUA4kTu_Yhcp{@g-b#XK z^*$H65Rv|Qh_l8Zgiim?^ZWo9>rC30cy*A;o@S3I9VK4XVyF;CkZ3NcO6-cZi4+}r zC5pOg*%pRD{~FU)Lks89(nVW=OxvCzh?cztM65vvjO>GU;YfC;vB%<*h^TLRr8*-0 zW{?ZVl05&VI|zir?sVed;M8{-pefJMQ7NFRI#htfQdsn5qAF|?#N|rURh(Y=Ww?kCL1c$O7WqTWGeN8+bu=HcW^{}rQ0|*^yj7h> z_UFY25orWM6d)3gBH$rumfEpck5?TIN3_RX%@^tCK&2f?J{}t9 zlPNyP?FP~rnhsZt0#z*jW!&>_AhvHTveUJ`?EvF zPDRBKD&5q%7=bY&h|EYwAmqs&(bTRxnmWF3*&&GH)ezweSmO>*p|Cq1i$Mr+5#3n^ z*aWhtk1GJF5=}ZFiYAjqh#r;ntJB;h2vuqlzbeKLShz?gOI@-I z7b8MjWN?b5NC81at}^=T=sjf3UNWU#C$3s?efTZjgY1jH>>IBuN(-5;;#R;JMNWi9 z*2ehHRU-h^2p;WmAt?68lQ~4u@+i_ThPdFWWA%8z5`;)g#izMlDx0PMKSV;O@6?Jc zbUP!o0yUYN^D7<81tC&)8u!2YnSkht@kWDaOj1(Fik6>TG8TrjHsX2@UQzTbU*doi z{GKQ{#$&4`C_;38|NG}*sPhg-GU^V4TE+OA+-M|_KBK6b5NWDAN0v2#6cxLo7I~zX zT+O%Dq19?RJ0f_-)*oCmPUWt%-}_=|;>uOAou;dvuZYbih|%G;7a7s7P6JdnM<_Bn zox%v&Arazm>;R(TcbUimit*DS_&UdN)WYr22Z=NKazyAGD0anfv zkyAOcRZsugime$qBERIiaklVO=->bD>5$QQrL#kp*XcpFa!oav3@BQ}CtZ`~;IPV^t zTrPXLMT02LyJtY;&CQH}(HV4~Kqw%DL1Yx`8DNOJNg~C9T-E%o za0xif@89@`@xO)p;oZKwI6S=ar={0ZMs$x2janyN(h6zJCQ-ZO&p%5Q7#$0P)T74- z0n&SLI3z+u;{;JM%mp`*P`8zH^AI`5argZsej*McgjNd2AEzFw_NB`&z-5BJW-|f` z%nyHyuL8i=0W2+j^FP+ED5Q;a3*X&ay1OqIE*2JS>)V3FfMy~23n`KZ@nK)|*(Y_% z!~rAB>_W^SNtw_*bimudKE$1hI$@wC4?#;q_L9dMSh-1wTuSBAi^Y}-rHJSTS7f{A z|7Yf(8J#2(+n=@&i<8WkbH4MPGv`15j(~RWl6ZeK`E6GZgog10q2DP@<7vl!1*x?J zX$eDpUtaxUD2`>U&3r~f=hMmQNxhHm`;Q;r8MWruCr#bYAmrC=5FIX#O)QaVIX`S- z%h_LC#iF~uk4Xq_w|RQdeBBK9Uux+k3Kq^ai0^p~ZBU7<$p|GbbA}bH|#MP5P zAxDBm(>^I!61hCjlJk;O)c4QXp(s@#SZG87E*~xY%Wcw1w{DidUY{CS`Qx9)9_bK0 znaA?z&i))OUDsBy5CVW!<}pjxbrOPmTwVw-pB!8gAp<60$IupndrDdZSz}dHAa;J# z6+)QczyI-1J56W7gk=5fzvRdR6>UwIE&%OhP6?&4vC?U290JKF9k&tuy-81;hz|zV9kCBkAk^{y^5CJ2ne}Ol?SX zD`lQ5yzu&Yr}zGUtlQA(!r9r|Ghp1aXK!&GWNh_`5z#P2_0@4@ZXPQjP(p{~=i@ej z`WFs96`#Ob0U<#xW7$OyddyT2EN<&VchAlK@x`un1D8=JsBc5m(_kkPMC<_SyF`};&E>tA>QNPt`s z^LS{8Wb-pQC!oU3uJ)aO_3dxMhHa2f?vjAwHUvk<(Pz}{zsvCp;^wh))ws3@Eckx3c8sdYgpR^AAx zr?-poAYb>P9cvFpBADmIVmd}yTSHmG^f zuf|_Nj=H)+0{tUGLXT`L{doWqs>}$(YXTB+eViUS8{0uDFRiPHyY3yK>zvKU*la@; z$~N!1ah7WW>6F9ID%HG%*Nj&Nkh?O4MxOFZKIo> zS*RDO);_pQwj}3jCgLEjH4f4b?1NJeWVF?pt^+4O>$RPL3!DMq>$y#mo6YSLb#xEs ztB{TDwag=I%OgcY4CAUXp9K-8EIEuwC`*|)$g`Jy*~8gpz9jsbT%yr20pc1dO9_+Y zP-j2GwMF+ErDTM8y8dW!zkc{=70CM6&GP^STCdLU4;0NoXl88?2|_*?w2Y|ADT#-2 zkLPH1)260O*5r!ua_Xtjfr;?bX)l$t-~@Uft2QQPgj zEIb{b!5s+yIr%U&JA?ZR!~O2J2M}Ro>i1XYakjBPZwDyQd9@coT<$58g&?1+(eNcT zRCpPrki~Ym>b7U1{^}-rW1D7@C}}0XX+o0jW2OfuRP>@1Jkj`!(e!zDFOC;(z%=ql zec5ghl+ielQGN5?A&K*Qt>FoUT&@u45aTmxFnvx5N_6xtWKo)*8WH3k>JIAlZbJ60 z->G8JZ(f zLMHWN2NX1xX`S*L6vv;^Yd= zSTk8j6KI6?jcg;-6hmz9h?~&`~8O zg<|~0N8CUnWK_TNZqX1zc6>~XXmOQXZZypg+*UhH_HIrgX(3%~+GEydV~-+a-5A6ZK6*D~oJebL>h@ zltq#Hn2%}5NK41oMLS!Q_Sa}=(6tfFg2)vmY)0cNhL1V%iRi{Ncz)rY^7ssH*P9Nz z2Qsl@R?CQz%hf0*EUyS^gSy#M!K)ksgr+E)6l3h7OaGtmneg`kA#WeD5Ub>r1`UwdgcMK#BM(KBiEwZ5OnA60J_I7ej7*ShFa;j( z1MR-~~zKj^50XGdhe5}C99A_o~kb#qol7{?Ps-ol?F`E%x?JjF&c~xjQ5Lz!c zXn{8gEJREQk{Sac+=L&Gv}z=@f9)nlAvcE$oWqA&Pn))m(Xfe;Qwym}5N$bp7pd8dV>*e%(h*Y-$Z!b+f*oWY3xuM<)Tf6ByezvJ zT|~)-&!`uC$b;u${QiK^w4*4!&Vboa83Z9;;~-^{tjT<7$=aOlSyX@h9f$}fBh^%- zq*@Mp0WG99QodLwv%%ra1AoAoz<|evA+lV}4wGH@v}PX{D;*%46O5KyVnPM3ngmZ$ zkytj&CX;F?N`p^itf2?C?dwlHCuZODp&Xy(%K}$Uv>ZuRgs=uClwp`163Z|^p#kqp z*N_p%GI8n_AH$LLO%l|M)({>pmPr?7Bo~dZI+I$e`Lpha@s2=p@3m|A+6|CaSUpsv z)+Qus{)8{aNDov5syaGk>#X)@MuyIgxGx(%g9Dxrf)ybflA=T|Z0WLv%>qQE;-`-a zOQj^neTXLG9U*{7P|6@4_*SPQ#Y#A=u|77D*uXRdAFNA&2t;Nsasei%`Wf(qklrGM zU5$7eIGH{OlHmcOT+O_dSQ7tnoiHYPN9b?Y`UH_fTDH|hCAO5ugcnPsQs89a>DlgF z%xWyuF^Y_yTV3K~*Ao$<$pE4)vo8Zem@JgjZRcl=Bp+_P;rhDR;PFjw4-$%5!yZ{D zQpp&ouPnGWlS=z#^Z|tQYpleUy6&1u;T&f9oDZgRLi6d&y%S2{rC?1btX zA7{cgj3ENLUM`rEGLur)ztqOfMz&k*I_Oj?#lv4sQ4sz;d3!d{ zw$;1CEe<NUD+8WA4FmzCQ1nC$&QkUjnA!;f!+e45upKI3{2T`;Ji z-~ao|?dWjZHs;Rvc@7@0F4|&&En<0g`$NIjBGzt9c9q-u`<2T}ImNiDoCGqw8ihVN)m9W^;QUxFKz!4LrDy^I)u0>a@KANn0OCYZq!8 z+l+m1*@vco#s`W#5KIj;DB~7hKod=I0!31SO)z3KO$f`3mF+r{?D@{Ux4pghcGT7lZNqY-dEJ}X?CtvA7C^Y({~nUjnk|75F^sQFzF z17FSH1^SR}#RCh=!4n^=9oX~>%m^=z?b28lbRVZ@*|k-ZWxjr>wLuC64fS(V;#FvZ zNzPeJnZlP+9qK$Q%sd*JN_#xft*tbR)?jbK`GM*Yge?aU@}+WQN8BFtdP$xgh6|wR zvB{^H&>c)jPJ*fwv#D~W@N=q?5X<~l>m7^9G;h7{n>owBqeJ}s!WkQxE7~~|WpNq} zy>z7CP^;Dxak(RY9vs!Y_F!nl?H&mQhlcEM=>a|dWs(p|D*2I=5leTrRNeW%KvGc8 zTC5LXFKx2i4s1`x!<{75+wI|j&Vi{dAM1(2T&VCgUhtF(S_m97b=w|IXZSi=8Vad z@iYHIE8J}BsVT30z_#u|o~RGU=X=xmuhFyz3R(Py+r-m+KL2O66Jrlx+;F0U_4wj9 z6EsbSg5=(e8liwZ7pBUFx}1CiBudCQ`7fc~aPC>NQ!Qm9Nu0!H=d5h<33S69r$dP9=^&S{jF*pf+0amx5ne?D2v~S zgxk%X_}-q#n1GO8y06n-=`cvem@QQb%8bmqb}=AbdsFo(R7w~R2fafLvZ+urW1+f z*|^7(y2T&)<_HA>I@#V}*`OrGY_*{H%3HN{Bh|O*n_5>nTO*G-6&1e5Yz=&O)q+jm zI*NR5Z!kE*9q!BJ5;Qjuil&!S^;YLL;Pa?>?lG2PKL3eYLiJG!^wMmcjgpBS^u29A z(IKl=TwRA9X5Pq*az@1+#RJw3y$}!e4#k=h_~uls@yTKpxrX?iJMcR|Dfl^z10$Gea6M?ZRRLhvn?#?oUPA*<$+WJV$eVRt#xaVbOCuw&yLh|?DS;okIO2VDo8W*zp1;cHkE$II4}M5-^u!jk zVZnr$fV2FbQqdQL~OGZ1hRRUCaGZ@U}UtykiKyi>U&%UBq-3i7mN5GN7P=q_{#(P zPztX$++v5vT^L$r@bU+*Tpf~*Lv6LYy9-Nq7Y9j%C{k!L&&|<4*iN-O`kh)zy6;96 zA__jAeCTAgVfz*bGMFYg7qGTrBg-kgsX4Amp72>HatgHjY7;Ba?^G_!TJ|5tYmV{I zNXY9Axg{fl9$0VVPp>RF_5lUjS>H{Qp$@dji%M<)M|82(mf*EPTP{x z;XPbdBkq)(;HLi0GlAAolujrO#`0kmB78Ra2==^;`5gUN^}S06_pyDa(2951YqN)K z!wqJ=JQ`TFm@<0neG(e#ghtQDctA2nRZ6i|N@?`YeusXr|AfOV2SimGRmfPEj*3~- zM;ai5qcamsZ|j;gyd>bHv3n_sMu`KeJ|}!0mKMOkK59`9$g}(U6q>f%Y_K{G2gQ1H z6B0&KCb^A-Mm@f-btghZz|;khI8&3lAyFZ4@Pre_rS&T9=I_L$d}?u^#C6&b|JTaE zc&V#6xI(q8!FkdnA&}wVI>JacU+s$c-1w9Fd#CsB^Gxr({*0aJ914fSu*D)$BYNG6 z^Lxu`VBr!gZiv;kRtccbc}4;vP$Q`u5)~+*{|FjL6!)x)GCX`3=!iV4cb+@AL4X@f zuU0!*BZPIw9l{H=p@FbHJT%4LU#RY$tMgebz5Q_og?==2Z|{dEJ`e1N4-N5G&hlye zad(*y_OD#08hZ1!S9;F(^z?9)>i!5LGRPL2bwa(Qg?~`5M2KI+aNdWr9A*Up?L(!<%>UjR`Thp61VD%eH{ITU+_C@$WM@N|J@66L(>7y7At ziIH7^47RmZAcWsZueegvc@K?=_$=X-Z0fDgE68n({buh6*A;mi@I}9VUas-M1ZUY}>KswaF_b>L_xgzkZApsK1JEi!&vK61zx~qX z`VT8Ivz@+6ST0^7lzIfG4VD($(Njv5d#W9#ZbAc@PPy_5W`+TAcnF!gRD|yvU|`0# z$?-zA{LYf_C-cXm`K9j8(&N$Y{oV7>LDQO_zJ1W;E48zO%#X@#BUB#hBt z#pxnx750#Mm~LsOVds?Re*VVwu_c=3l*}F6Qt5~eBF{sP(2BGyX9Gf9Mm=YJ5@PO*noWsHZ=X-o+FA&;#19mZJ^=Y~uh;HX`$OLbR$?MjUh&xl9#ZLBKv zxk_~iC&VzJtiQFfL#E!$p|23?44rumgf7c<2)k-G+4|rewF{>NHA2r$pTVN2MipWCZ*FfiGMR7DhDow8>>n~_zt!1Hh+K#}M55bC% zV+RRFKFIN5acqkqkLC~p2{w2)hs>N5L&PkDy;*|@GsHMP4aSFYCeC_ow%)7i?y6RI zJMI#>*dWp$zxuu(_3AYtbV$O%cG00T2BV?a>w6&70-+WXN_LnLd9>YtQ6h<&6u#jI z-HF=U^yu_5=(Yak?-UP-MhC_9OO47<0b~eL4!Y||M0C}$^-O`=<7P?%H&olw1wT~} zLTLB{5u!nQjlt&3UToca4F$&TUjfkYkVUAONK!^g-i*MA?rsOnDpFJ&RK^i9t;qNt zCS8$_xU&ZB>+ay)mvu_$&2>wtsSvGk^F8=K@G8|P++-u=U~1V{4;rzdq2a?1kaf_p zmBLmY#~L+_s9{x1Lg7XN5CNo2lA6&3V?-W>tF+w|03o4)sD;S<71rcPlx1!!h6WdG z;USF02B7|0?R~?98iXkKtjoN_Y>OIobM7-isI26^I^#=+62oMheK;7@bM__^ZR{h| z7h|!Teft1t)4F(H=t;mb5oN5MM*K2gjWo=N-GHTpL`3v1E9gw5_L3*V_|VlfX&hlZ)ZXjZ33%LNp^}dc5i+7~Stx09H-iYF2>*j96PX7+k3x3G!)%I8QRk=S5~j6i8%SF93yH+6OC}kAch+j3%L7Wok^O2!se8(q-i#| zULu`LCYe<0;L2*??4Qo{MXXz#P(>TjT$;>jW{bIhYDvF*+TS+2OCJB=VEFLxaCq}? z3255N<@5QR^s4XcDi;l@+?DceS&gf4gk)nh)O4T45CuZFaKHn14^Nmi5biO}#(QMnE18(pQD(R z{bAE-LMRkh;}nt_=LIQ~*$p@%qD%}z3|O>d7(*Cp1{wPT`iYhi4)qFzHc)JwuMwsU zBZ3Pbew`UHA}NkGg0VH*Zm2tN=VnHY>KUjIR~_0e#mP!gJ5qVkPHY4M?p74w37J=d z`Mb{>NrMV}j0NA_3_B4bQVkK&c+lI3AnMlqIkP{Vxg_aHL;`dUKbVC63qwDlHuMt# zxLFZ~eE}ggj!VUVCMI>;W#|$DiP#8r4m_!=%x01T9)l3hjOLX(QUj@WyYgZh5lDH~ zpEJXKrVkld4opD9|LOakgdQ~I*sDlF9iQ3|qmw|#6Ab+r zSdTNld1Ehtt~sV+LXGpvum?Q&&Vy6lsX zuIo}|&qnAEY(a@V?cdqY5ceh$s+Z)reNjrKTEGKQ|80_Wc#o#qO?2Q<(N--&v6~zr zmb|*$n^LB!5kZ|cwd5-^qfMUBW1AEhZTo7c6uhR9i-eqq<&-d7 zR}*G5fqZ0{ut#4u1MYQzpRS`nca`d3@qQkRXyiW0UDH0@{bv_*E2-dgdx~tOd}eQx zxqA9ChSzgDQvUn3u7|UNQr8-?Oz_BZ2c|~BYYI7z4e_4)R_IY#EE+e>2LQiX(2$Tc-*KiW+`S{=K-YaD<;8dY z-2DZ1n5Zb}rS>9T6p^01h0}4!qWjv|<``7ZVnQHy+Y9 zYwTL$9z(WPoVTM-A>=)Na$D};LjWP6NH<0x(7P-UBHPIQnzZtBjtJF*$!sv?H@g>( zIZ{DwVS0HBO{hvF@!bR3C%CpKwRl1ySr#Bwm=!fWr+?z)m;PhMfrp#`Q9^xl7^A)B z(+*Z5auuSY_KrkM(E5;C*8K!TpP$Ryj>?o&8%+p$1-E`4VV#uo!nC+;Ldbwr;nm2K zQ2!?pDlv0pGDiUsLd5joFsK?6Lq=As_L$eOoXb5M885?Z9X|olm-em?5~|nHghqSc zbRSI`ghU~#Kcavtj1f~Kr(G5iXUtu@HgGH7&@eBW4(7a|f6fqI67eFlgcVTS?e{FK6Dof&~|u@5mm zK!{cHM|*AVzV-wno`?5cFO?V9_56zm>7c)J*VC8mJq@XqEr^Av32Bo|hoA*_LvZ)9 z@dy?q=ZMinM2B=)&Me|ZNaRBZwT#oCUYj#3&}O{{uJZ_#F_L;bm@N7W8EL#IgjgT3 zQ*abbNW&f(?1U4gx(WYj9ual@@lf4yj()_HAtpo-*Y|MMABE_5^L!W~7F73|$I`}m z2TR{wR{1+=3g$9vi_$4S@uD!(lL-FKV|b%DNtN9pj7Iq=@ zsD|&~q0S6D^2O}_>QxV^E-vMAYea~A1l6{95oH=ZJqf3`7YL#9m~|I`clWi1MTmY_ zSmcu22aH*Y5H)&wvzF{gh<-0f^(abppFJS*km_J?a#s3&vYhhKh(_W?L59ljQKqj~ zNk+CB)HE$?5V}g78Aqtl946|R?6d#9*=gkpNY>&}s8f1DY zRVIrNGVUyjgzPTI41PD<0Sy@2&BKVo&V-Dwg_yA_3Q_;9SM>J6n8yg2@~SkO4zA=* zN(f}g+GL8x_SnqRo83oB5U+W-5vupt|H|yaTHb)D5xFZ-xZz0X5!<{2ct$0p_jhi8 zOFPfq=eH`U)O0?#lAX?2r5e4 z2)4S?*bt);S|~~j-zvqr%4UQz^jjxT3PZuK%ko_eUCd^fd(XY^<-M2tZr+QH^{ook zHt&;jKIiv2JrLb*Ew6Be=(m|SpG`W|C~TeAlF<1}@qfy%Vr9J&b)+>R<}lMEQW!IG z*&`WZ+kYlRY=jhvxdFu0PhquA7z1Q?L3FwLp`TCHu;Bdn&n9#E^4*S+mH$!_I_Oi9 zkP@N884W^OPDRnZ$}me56b&)2GU){QOoWD&>F)^gNuYaB#PItZU~#~h@u?bdb;`_@ zKvmc}@;OYBLK9!avN~U+KuB|?E3H+tzp0HGXjp=MheRKuDg#jMhK9rMiJFOsglx1q z#cDSWjlqv7iY3~-A9x`VR0owNz@sT!%7AeeHhC88dAR)DP8pEr&WOKUpwrzC{- z<6kxJ%cczSg%bgaMR`$tlnr4zS16yAO2y(>KA&OvSjgbNC)O`Ju>-S{6gwu*>Ke`rxGEJ*&Zme6BVU_Pz0{n=xLbh0#P!e(+H8y<#L6> zV;-aorF6Ts6JwDwqm8NPckyxr##&j$t!a{Ut~UZ41A&##Ca# zP6rMT7cY{DngXOKF-B|D7^{5+agmHuh+g^2HbT{ZdLzWmgdL7vvM=;Abrn%S{~g7v z+T7Cu-HL$R(H55#b4OGX4W^pC*m*Me7ZLxIkxRMs(oMv~S$j-yAVerO^U8!lylOrX z;R|N%x6BoSMi9YMmPY^IAJ40I&`vGL2{iO?+W@0PlDv}Itwz+ENAQdf4i1jL${KuU z;!*};NuwA=;C3Sj(Zo?$TFQ;68d5}jpAmNu#XHOtf>O^%Mt=K7d}(An3%`!Anz**8 zOHJgHV1#rC-<2#YgeZ^Td5qy19DlNtFAW$KiBPg~kFn;@M(AxzyqOxiM0S@veE}&V zzR!s5y_v((*pxkda}kE(&qTGImzbK!(4k!Ows~mXE074kda|~*v$M0d_T=&7ae!y= z&f1H-);=41?O2irQYaLGD`rG1!+4hj`p~ToiBpMJ%_pLVuV+l1-WGOOUSB}4G0U>^ zDvuGlL$WvO!!JWVZ20ek%4hrz5}$h-&)SIUNbjDA!Nt>%rWzTEM#^jCUo@+n{N<6&`QO!^U`5O$&|@4Uz^yr@_!2c`yA|*qt#Y*jeZRSX=U>2+&olF;CP0c-P+~k-zAC~+gy=kL zX_JTusJe!lAtEvnx_1BLn*`7UZ0B_@P!SqJL|rkHu7on8qIpwd)-Xz$dW48LK9Rz| zzFsV^wcny+z|zg}d(b zs8aFl=wsNpIn&gF^2M|A#YHw7g(srrB`NfvLPVLUgA`t&>6IIr84t|dmM{hAzWB#8 zy>kULffn3Xr42e&d1kr(nxRre`djh^^9ZHKWY=n}SS*#!%9n*S6X{4oVj^PBY&~?w zWp0Xlx5GF>ZjS^|pIFufNTQ+sO%=nr?W)8@SwDg``4lvD%1j0!b;+)B6QAf@GG|oH zg6oq%MuuFL@Hr>}LyGKs<|WgRrc&El(iZR}R!6Qzk` zjpF-FyfiMz(=($j)ea00)HRS5)8`<#R4&nw`j~FBMak8naz-mb@TXFpG<2`F+Ym_v zJZ=vWA{w1_;A_Bmt=>8ZB4eq1hu6M%1OUBH^8uQo{Z(7qOR)|OS1U@?3d2^+U6F>W za@4Z0MFW9OhYv#hgCP>);}fhOgII{xm*#aVLWT-v6@Xrf-=A?~VX~Jocv#hD>$a;5 z{u#S(%%n97opHK8v`Ut6A+Ljk_=}!qx)%_@3{0JH@sK~4{BK1Q(zSo~YJ zg_I1@3adMRw|3x)0LuOcYuM;%*zUU>KVDeY<+aImd7H7n3VECZi8B3aB0*!O@xjv~ z25Be+N`?tT9Kws*PN~K3Y6mHT4a1D^WBzq*B_jaMZcH3}30qtas=pwL;?D^ zE3l5B`c&mE>Q_rPt$3C*?=IdoOhed`!K*s0AVjjo2Z-q2s;X~(YI3`#ttS8>0Xpv! zJ#O6E5Z|AkW_zKuP!{~KE!<}1cH1oK;7rz#e6e(yt^*%3lVym8kh@5$94@#Yge|^P zhG>6_LZqSVNB>N6%;Igl00!X5@> z!C0e{;(}hAVmrD=ay}}bh|H@fujMQ8?ZIOY1ReDvI{lNkBIar!0 zMEL6A2?L_Z>D^lHA~N%2QAJklbXN9v9|twdi^-}%LejfJyI;pX;zU}KSJtZY8A#)r zYq6NmWHM|eivMfr)srC7 zS*4;)Jt6@0AFm2}5(Hmg&4I(ZWeMSg3$Eo$hD=5R;0(#1B7FeceJ#>aDO-13SUo0t z;bqtH(L!)r6FM>SK}P`4qTSb`>_dZ+_w>Tj$~>J5U4lbAh=TTKcmOrEwAP*tzamb| zr^3_ZQ%FMOtL(#x3n>XAuxL|D%fXXXeyyy{6NW_|7$DV4ceR5f8eCtVTbg_{xwNoO zx8xu0#)Tekh0>L1ln`+XBZ+|O?5LXxk;FtYM1+eL=9ZT5k&R#tp0>5(?gbQzWie3k1G%;r$ry3$c*EE51nBtb~+boONl|HyHcD67U z5XJYkXH#}-3ee9IBBiNUGf|)1=s+P_J(R|Fpu2~sWJnNUFU_fFWyB>Y_(&bAPJ?o{+jzT1#gV~Jxxi6C5fN2DaTW7`a>RSaw7$9R!B#ky_ zY!R|mi86`K9;g@`AUdYE4?phD@RSU>=I`+|!hHpoX;*u0%QuRG$0?%#RXNtzB0}D3 z34lD+-SI2uPFsr|PnAMugVnYiDrxKHQ_tNJ zWXxWk583OIznkPX;v$)z9@sqiaK`B9kiEW}$Zkt|(Q0cHLfWK$18LgF(GyHWh;4oK zy@S_Y?{6A?7d+sva~f2At39<3J@M|u()}iD?WK&Hxc1h zdPf5hYI4OIO4eky!NOxwrzMN4ucHs_jTD6&O4fAt!|LXzs>sk;ogPu+AMmUjiI2xy z!_ss@8V$wwzMF}V!<%RbTZhOzgU4HwU9L*i*P>@SBlK6WUy}S7U|$!RIXhppw%kmF znry8`1~*NK57}^O2p{ZK<10rvcy-=Tgqm7>mVmM0O*11^9-qM%c3R-( z2BR=dTc9m}n@&B9#*Jf`u;IXoxPgnSiHWDs1KHL_5MvC82~B(p3)7d<_ulY={kr5> z&nAJ;Vgu1Lq(WoVbkn)84_m#0@%vRjcYG`q5m$18VTXGt5<)IQrb2U|&2UKP_F_SG z?T4UE?;V7VNT($}2ELft$Y_#ilo2VE>7Ay4%4;~%q`M<;)%@%{c}DWMv zOGp)A6vus=w_HS!6ouIXU4-@^q#%eEQBt{)0vFn{MG1n?1Bxtyb`?fXkTzKf)}{!g zMcLwJ59mn`v!Eg)q%uP@(qD@?!yG@ynQz7`)Pc*LIp_aB^FQ-_XXZ>=+W%-k1+ZWO z{HZ`Kke&)LD}W*33s4e+p8#&-*xpnKx{qhif!v7v^NRlim^~3O)skpLC2fkp->7)M z<^R$|1SKU&2XI-A2Y><#ZWi!bI7y&Uj{AW`95F?zx*A{x@KerjSn%rwI|LL1i`1Py z5Jymxkbk)q0S)Rd<%IBG3HF5Y-EsF~96`^7d{OxdRh5SWxaI2ZmMMR&+D{o##N+^f zgj{XJ?uoxEfZryVH->+q61pvlpe>46WW;uh|0}QxI101_kAds5tnOwW(~&@r#5$q~ zx*()aKCueyZNCryoW$Nm5%fSvZH9jh@GXL%Vc@KhW2?lzM-lYatPp1@h9Zd3)`8cl zE0tKgjSch)sRl>~nqvyl_5zp%lqg?WxEe>$fDo$iIwlp?&FDwMW?Mn-(mYL}7}wsb z<0@N0gOeb{&#VRY`jCf#PT&-<4>$<4_~1SRCxK%?E6y^FXW{b|&L-|Lh@HSPA8ZD2 z*T63K!Bqk;eF`kJ5!7H1TL@@Rpg>`}$HU@GjyH|mPk`(oG1Gu9MR*8gh7^z~)j~+u z0J$Q+;>>BB|ZV$OF0-KH32n`naO2HC{w?{WdIffeKyN$5>EqbBa!U#P5~apA?^h7Q!Pkx*Tf-B hiWtcq_`?3D>@UB%dii!0VT}L)002ovPDHLkV1n)fgx>%F 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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 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 zcmV(mK=Z$eP)GoVqAVyOfNM*Ei*iiVOuRTJbF_|ds9eR zL@~96c05R0-=u5!-_zu|q)At6@!QG$%%1YKeI_k7@Y=}5dST|frbt$5?ze?*NhtZ^ z)#0UV@7Ke}d}8prhbS;Q_ukT0J}Em!RzOT%qIy^Cw1UjFitp0DVmUKeK`h^+Ys!LS z&Vp$-LQ-{4E`L%=q#j^9hjDS`(?aR7y7Y~i+|C%ovevzzj$7wd2g+5P?c#xs(^OQub{Y!VpvT$MMzGGUl(pvJeG1$!K9Li zTT+c}Pk_wRx{!mfhkj*CG`yjTiDpMiMn8I9MaQX`zMpxSVMpxGwS-qZUuA1>cV<#yRz9P0NV|4fxs-K%Rv4g)Z_bus<ZbUnKZ)>`S zO?NJk-d)i;5l*Mw!pl#%I)!v zxxV<|(l(+KKuiprW~4?0PZ1$-H$6fY zvzSGP39NU|7*EGA(R;$5VH5-K6lA|jD+Zwd_x zA%SvHaOYofpLc$k^xXbXn|BuHwFpvp$ItuyexK)g$2@v8hUJ)>c=UH!6Ept54U2m* zV2dzrJ09ZW>=0&R$_^GUXM-@4b51*0rU)B^@y|uGdh*&s3hWNYCtmo%A`*bl_aKzsma6Os=hY@zMOzJEww>b9ry8?@D=6tB$z> z9HFu-JAO-hp*%tuRZZn=>Ms*B9H9{aGw$vWP1x@RNi<&OID7iO1FC;c29u4u$#|iw zAcy664Z5){7mKF^JT6C!-DM!l3`CJA@O%!A%a)lRYNpKdGU&pNwU&9I1M3ds`8}28 zp4!wZHG$7@giNrDuJOe%fNYB##-o#B?HFQE8OsUiaWYhZ@kPe37kXV5on_93OEO$v zWM0rctwBb8lVjT!vFKa#g2u7|7k(BeFbD!XQe%mn#*Pcd$D*di^4Obv%{X2Ybn^%Ew_L8qJRI zPX;&{P||6rHyTI;jb`TQIAnqT%W3g=I-N!^U^47ejN36yXhJ+aIL#FhjEvOz`8MO{ zprB}~kWQa&p8^aICc_{8$1;%v6h%qP08>yk0Ad3_34>mTXo98zN~u&zBb&+i!|(RV zVCr^0)5+&lG;PRedSYjOE~wD*VOL%_EmbQ0%4s?+Kvl*be%FF53+X|zTJNM~G&=DG z1Lo<^rG*7e{!^-a>|gi$CFC*)4DRs%@=Vs!gM6nh)r+WG0?Yxg!+0NDnkN=vOl{@! zANz-gJC~P-mC|V(Rpmv@o!?uN4^H#NhA4{lQ&}~~C%y)Ap?~t>4r7lpnVtXc?A*ZG z=~qg+$uI}T?W@VHywa&N3=@9I%h1wfod%TQd=D!;lSr0)y-~lqd3kkprNeB;g#zw7 zziUC$2LH-bB?`gpXtE4~XgL$+VSh1mB3bH^7=C^Ax?Dyuhy5)u8G*UOIG!CJDVa`P zq+l`3M;Xn^tg0C>?BYXQF$ZH(Li?zvvCS7m2a-!ZKdOQ=YD7H zpq)Y9CdpeE>eE2g@3v^g_wENXxd159fmKTwlorLMo#tSH^gd&CXh~?_CUwJ{`yIej7mIg^$ z4<<>mS}b-%=m}J{&`SZ!+cTXd2$A^#jN3U)1B#IILx}Q}!Y=MFH za?3)RB|xf|!#Ye3p>z$DV6eLmF!N5=H-8g^;yHnPJuOfpTZ#=zXMxCEUT?_+uE4la ztE`hFOE-m%LXq`7nvU+8x&%X?J+y~@vM}rSPB|uAlkUH6g;1K?8}ULaJ=~5IJ0fXJ zWgyID1z2#6WEKm<3`vaP4E-nxo)Kk|C$zbqOwZ3R%=)}j6Spq}T)roet!8X1n=R0T z;n^^q21!~I=GD!i1|0^|McFIsEMRK5Sn4o3iE*Tmo_2JYm|(2gJUKbpee-rb5sgMY zh-ub8<#JCtoW2G4G1`nIH@CO<>Ecl3PKMEMMX2`!%+8UF?NuzW767WNi7Y+wGozZl3J!C!WmDE=+rnT8Yn~-R9QzdZK#EQdK0>c`cUm z%|T7Y^#^6IY}7G$o|MTes8^ak*QZP>+=;L6ezsU{7h-~c!AR`vy!j@f8*d$XF}b|i zjqXd9Dh;Kow_$TCgHh(d(@SpXjT-)nlal4dkIY4($dAaw+!0?^SYFK(_H>B$B60l@gND+PBHdq6f>fE z9kQw;QyzQ}3Yfx=SUTkG6HS{l<&U|yh$IpT-ghN;6Gro;BynVrp^W=~cyIw1GCbk; z%17`8*WmoxSyD@~c5f z-i4)`80!3`G6u`w^jyXBX_8;%|!1J-ViRs!!Uc9|ET~)I0pTl~!@)wgOliyY)6k1(b`}5V((ksw2(yW>&!{K4X zV`U{@I!p9saa>_s3$=@Z^`ZgVWA@$ezay5lFIggy$ah3pT|p$zmzI{+La{BYiczET z7}3{RD$DCVEJfKp3q!wTsZ!bo6a@2Xu^sbeA^E9Zt#&eK>GQYIU+Vv(<$6Qf z$kXs%Gux(XTUd6NJ^G>054#7;ElU!#Xh@tS^uu||L~KJ?hXk1HEqZ0G@Wk!(}P1hD~qVCc+~A3f!hONKP-WM%t!D2&E$`k-r5hz?}Kg9 zfb-;epXYtvcZLF%SbXTyx}?jGp8g$zvXmt64M?m=#I%9@LlKfBgAq6W{#e{0kJeCD z%Q7-~6uk}A8e5*3c}66MKB-TG0W6#7H@DxLd{$z`6d|ovGD0><#~N`~JePP^zr-mE zWwFGZ4u?DBbp9M|F3ZF)xV$_l%SWcA*{}&%RI5YEx+LY_{qY~)kE7r6=j4P#C({zK zHXS*^kaWpVbB@DyjZ32$PUQuj=ZkRGp(LH|Ul*H8vagJ%rD+v@j?c`*@Kt?%u$=t* zZ{KI&_w-DzI^8lWZTnR$^54idH#&|WytI-!$rx}XCTJ-Zp5=I+{2FJnQLtWp6l^_gJMO^cq{t-y*Ceb3PI zZa%*K7A>gEKuW>t+a`y6-0mVv(=@{{Ifh|HnuUqX8Ts!EX}8ndSnZXD*@}%JEc&|K zf<5)o)9(};`*-`+r3j)@S>l1$hbgg^48UYMgvDxVL&wqP;Uz3$d|{!$Ff=WIHMoP; zh7s_S9|xN&s{^^l4D(3(M7^+l@bS|-nE$GBwKN%85?x2JRI03m#VVmAb&?00);Yi; zCfCfJoh?bgA_6hEE=azVsz`5(w4%q6wvPt?F6lG-a->Rq1%yH%3!$AI0kPHUN zqwJ76(-ClQ-2ZXyKI4({lO-(1LXH!}9K1ubJeQAWH>J6Ie9<8H%XV2VfJJ|?F58O> z8wiFoZxbp4lBU1#7iacV=P*g|qK*VV?w&G>%3^c5I?V-E-~bC4 zG|h5?5cn(F$Qb$u2j!)4oWS8AHjX>xE_p_uHtUfISXUc$0m>W1ax?aS?&l#Io; zhG=PcnZ+MHa(ZxFXRY>^CKIkAoWXL>v0B|K!e%xu6~Mt0xed~{$sN3MCDleoPeZ)bQH57q= z2Vzv3nqZW%l0kPl=(}OI zfvhY;i@6d!5<3UMlFth)FK}glHWx%*cw!ysk{?@WY_4<{iI+ZG`d_sgG}96m zi}Aj`Wy=Di3bO<-oZ(0Y!iKVYa*Ma*;7C_pIs7 zWZ++h5|(AT#1JpaS`W!5&ZM>#gCLhNYyiG3`)J=#hhkWx(QZJ{JhFp%dskL^gDUb+ zUXp9kpc$BASRoc82%Ejrz92%IfdKJ3n;kD<*_@ff%Qf~%ZLysFa%?c8=7suwrLLrVavcwC&L*Y1Kv8_P=XK29E^p}8KWrp_gnI~l| z%GwV6AAaGV@17p>wQDyp47BfRSy1j>8P@h_G`GHuQ}44m7K(z%#9LYx%*I54p}~Za z3FOyfmX!j^Hk=C@Qt(f5mBAeL+Ed}BrbF_n3s|Og(Vy-P-%4S325`dO64!V5H9b)% zvX_C~>#mEGNx=dgVl3(|g9Er;ZD zSh}}6jm`VC!+z|}E^aZdfm~ABXfF^md^Z_8o))kzwt%|H=zX(m1$sWp|19DLHUAvM zXPYVn4QZlpq#w;L4@%3>T4OnJW~{$;-kb7;QHf#exXYO68hW^)^{J!C$pSma5rgO9 z>cthwfESirap?J6p}-)6mb^_|HQV6}F+mEQ%x2o#XEXaH_mi-ERzuzUms@=)?3GWe zNnx1Z0wo4=dHx{WeSnB*$R0}!o`-BN=511AOad@LVAsq`G8*6|(FwFbgM!#Im%Xl? z)x2<&zaE68W<&DHFT5G`K?a-lg*D(}*foMOgIxCR3`f;!i5O7KaaOj2b`~p?6!zn& z%d-VEUnUArUy#ufoi53`A)yAZW?%mLV!yJZhIwCkzE3p5}Uwn z#?i<6J-^0xpjJzYnLN^C-o=XrZ7q1pB^4P6g=6KOZ(J1_R?H=2C@M+*@yWTl%B=&m zyHM8Xzt%bZIhOkIpnDrj`+5*9Uhhqi%UVGHFxAryyr|o?8tsE@Ja!Bi0kE)nXpF#1 z4wcwaDZdnGo~M^eSO_=SL=HS2peh1H&j=}N6#CDdQ4Ag&Ec^Y7P)%vf@BJl4wfyuG zcIC;7HIJ*u!-9@w&xd@rxdB^fdr;I3=Q*TAW7lGZ#EctfJBhc zpr0$EPZQAdMdSk&EKWpBe21vIhO$P1?BrM8ak$!Be#K%KsYT)UUfb^XMWfMh+WShz zqV7dtM!J%=SnU7b4#zDV>WFBtltxM5D2C%yk(CzAMqA~BI#5h3a%GuNqN0n(J+c(!zmz4#?smy)@s4b;&zH6e&b#~M@z#!!h*K2G@sA& z&@LgS@txHeFFo`ucWlHtL->j-_K;;aKSu;!|UF-O`p3h3>k~`<`=hVp4bg zc!d%O2U$tAu3-slK#O4j$C(_Z?!b7}vX7=^ zip}E343lM}iH#7{NX;aL_x-;763H~Q4+VUgrt{AV=h2ng4^B4nmsUAZcK2A4@gSO0 zoRJikB@lB+quJ=q`uZ6sW5`A&@rAp?%fkzilstI5*9UEITb`xX&KrRw3k&f8QP#K- z(j;VqfBo$@Po6;LKUTIDBw2%3VbTYl2 z{EtnrT8sSx`em0+xEyTl{0+L+c%qi}Ns86z~6k z*Z28Y?w3Z*6w5FpF4ckl4Z@gJd{boCx}J}pDEmv!{GzrnQCjxz9!2$U>1vFs3Z5{903uw`3 zhSZdi9XXK2nlFr(HeZJ9KYd&a!}T6QEQwMCSb~9MG9VAcr6Vm(N@Zfez%JwGdcVix z*Y}umG+nqWkf%q1W?BO{1s2Wi-zw**-idDZ@`1Y^Dh2Hlg;6}8JoXNGWjU(%sBa9$ z%vdZz(3C zSFZjKVYK#~M=J(p$F${&8tC(J3Xp-6TwG~~Vti1I1I}8aF{sjZC?Q`M zX1N}ZlsOiQUxexM^N`7IpSm|OGzX0fqz()$yid0xD;$5RP^=BkHy$JmZB{ji!c0)d zv1MqE?~+&)_#s4=B+M>Wm#PV|WSL=Z2$K`?0O~d|OU~DLY2ab-htXb{uYYQ28aHBX zZP2i_w!+^d)QeA`b@K;BnTpQU7$nTgJIA?8QYGb*h1?_e>MW@|l#Q$H=5}Dw3}u<` zNGwYr<&cI`L1I4(xU-)Z4MOeqJxWF)9?%Cb4TPz(Up}rd4;Dt3C(Iyq^mCXB zG!!FE0EHy)Cq)$F!v-(?I}OLOVA7*fBEc4GJVys%AHx%rG2gUsqx(9cGxqtt$o1tmVGO8w1b zfRMw~BCW1?uk-`;V<^VR#xP6R?+F?~mF=zBOG}ycN9Z-<`mQ3Dx;+zev}DvEE&*p0 zpPIWGideYKWXSlOlUY?@%QvW_Y%WzwVchO$Hk8$gushRK{ zp~RG)cgzUcQup+Mye#gN)>M^ncll+D(1~LkQ#MUAPLCf#Y;|jE_F~3{)-1dvyKE|s zGQFHgvKZpqZy$Y?a|?js?rYKR!WmiX%wj3&(j+^(FIQP2zB_)r79_&S};tPg-ALtcf^=KVqjKLUUAe22Zxzj-A%vKm9;MZDmr98S4*url?qDC zv++Y8U7oF$^a+WNc8Mc3Vyc0SLF#)uI2Mi3SOzSsTiX`}S!}kY*U{`ueG!Z4ewN+c z0s(W>whRhL5atfLWrA;*>}N6S>_WYFS-EQhDwP+qfArAJaz*)qNnk-y?z|vA`-S>3 zG!k0QlAD^MY%&l9mfc_5HaBhDOJ3Z*Z(J^BA;a12w{LT@aO>l*5X<-)zG7l8ViC5K ztY&nzror9L-4dops6+N-rB%)uK3Pa)r15f5mb1T9KLlIk%1fxZIl-n0hv5%zUoUNr zZD!D0zOpTC*DoLza{o`C@h>~T^6^q0ifw6j71Eze{vspu=4vDanGT5X7XZ*z!3p0X zUHR-saUt{f{7~KUWhwa(Y>Zau1eOgp<%uzpr?cy0W1H(pmp7Zh($|-(PPw1q@XpT8 z+g&V6Mr7Gq1z~RCi%p%4g)D-7)#_+(1CB5i4zjg-hfA2!it=IMO`xAAV&xU$vs34K z!N#~!8JcTu{fv`m@ZdSGILAx3;#k zD+Arw$aF<4W^tr+Svz$c3v#k6>cgEWUy$?(#YK+9+YWC50^uy%H4AsELpIL_W|zIlaN3C5H*2CXzlT_UfmmL_8?FP((pG&T z3JFS?euZeZ{xdcjhD7io`Cb5Z4M9rwHIL1rA)S^Pc4Q)j& z{jh7*g)X+>LZNO{TI$HK@q?qLl+jh=QZL)=InVRH@%56}H>Hp^Ht#Rz_J7WKo|GIA zAWk;u{jA`>=y}H83$A`Uzu`95zAEl;uq>m<;_9oZxpRxd0`~_jhfwf&h0}Rm1utBM zS1h3gdywVB-hXaB7I=w@2^?a9H&U~W@qpNmAWFu`79W02@SkBLA1gk#^)GVM0w_zC zMbX|~TB>z*mVlm*GCnr*W*zQt?+6Q7I8;R$2SF@+y$vY!5t9A%hZNgRx1 zGO^*i&ybh=e29D+E!ae92w&#snQF%Q5kiVgVG4kwD5WP~qxNOl17i`9gLPM$B^1#fY*@ z#Y=?PC9Bp6FxzlztE*eS{FV&VVdm1YMrvKY@U;*3o ztp9P3{sTw+OeIUo_BP%VmZc?hF~A=7RN1lIpc@jb&)(T}^f0}iIzvn*31?D&7<$rN}h6;cAP|=c`Pmrt;$+v~#$*{_K4HS!oO!b`Tyg_y(~Syb2`(IL#K^67axTEHgDv0P%UKL>`RK$+6AoVbb`vP zAdZPX30gcSh%9VMmQ!`_7qcuWAN#8{Ru@AUi!u{SL0A^wZNdd5-iC@!ffyFHUY^r= zXD94dIf{No(Wnktv@YyB(+7U;$tdq}*Rk4iUze$H4WL4SrlLDF2@BIxLK11}z$_j& zP1$8})?8j*{uIxfN;AaEnJYiXk%j4260=;($r9H4&@LAlHxz`X@ic`JKEo8ta;KYH zPec_zmpzlEwF_aOk;TN93U_g#kCD&^5^`9Z6nZ?=$ks14&t_1(qkJiXsp((Sx_@9CnJ*C1tWI zJJ`~|Q54Bz0eK-k)K4R1pM}-=;<*VNrWs?gr?J>v`kZ{x^G{$|aZ&f66G#Xb&k85@ z&+3Ac(?f-J4JI-ZvS<}W+IMKn{ptuGw@j7d;{1h5l=_|9(MaMb>Jio#DTBf!E`REH zHYtzDGWf(qV2nmvY%u+FEdTo*Wcj|6YzLu(;agXUS$y^1ke}lJUN9k__Wo!ubdW^G zvY>$De*7YijYO)nzD8dwN}}p}t?Mv|?SO*c=Ao4Chl_!U76^8D-W1Ui7Msl!YiUk* zqL+WU-WQL@cM`arn(P84znIh7Zjlutvk#DzE{vKP9DLQ$lk1!f=NK$s^_ zc#!T0Xc*mu-e93NQA#;1j5M}rG-`;KyCGz;;T5ZFKA*OYRPWmb!}#hw{caNZ4LP?x z1SRES7X;&S`J`@kK(3LB^%e!gqFp8fv}1$)Bot6?=wgzW-g-P%O20Nk$u^EixTwKY zc8$3Xgr@$x(7s(dn$M=SOQjE-47}{FtFycRRc~L|<7pu%D@@rxl@}_P7ycp`A8=s6 z1hj455Kd2Nz#OiKpJ^A8#jZ(y3bv5!yT1xn`su-Pi$kN))S1n=k$kNqq@`vtnSKnW zUn5_HC`XtbU!v0Ea=Ct}Zz3dM7T!53k#)0j`K-5iV!(>B&tPi{4&hW=K@{1s3l>q@ zWR`k?n7_AET^*2^%@%`Rzc>^!8ErqNrYstzUhlVgBR0k?rily_m|USNLGFDwzPp3wVNvQywD3=V*LBto9gd+`>fWP1S_SL0W~yXdOQ4_#)R7Lu!0@LYerMKxegxYI<>*D zW*gPLpeR~xdxnW+uCTYZ&viHDSqw>{N_dV>zO{8|da^@MT%31Fpqs|AJ7zLWWOQoj z8Dk=b%KGk+NaTLm$o%~L$bC;=_uE>POms}th}fF^5|+rgSj6S8#bS5V)m4Lh)j!n& z3%RTTgCVAE_s$wAiXv2;_P>MU7kGwc?tV6Pu_To^I40lP8k!vk|H?am>Qr9wI35DU zd6FqOL^gsz^!GU#tKj|0qP)nv<+6FQ4H=-y?spEWIND&}O4KaZDx2>Htkod0ETFU; zU`Q{7 zi6MdD>+`4hxP<8-=UbIxLoA{jg~G4Ei0R_06xq~BWMm|={ElAB@TAXuOg3{fyB8kl z2JLXm8ixbZZ#iaQs3x(t7dF-`{(wamAS z-&v#y;_v>xK0w1#GYGU?QCY1+7|U%`#q_~)oKAtEcwNe>wD|jA!D((=n5uHRHaq5m zUcjWSICc4^kftvXuD{_ji$~+X zi#)8~yI}^d2H{vQP)3+_daRxExDwo8Ag;lZBz|5L6h~#$#&$}H@;RQ&-umXxzd^*P z6jsR07z{6PKxx?)fd$brW#Cp3*;4Huh|op!h0|rk^C5Ox(t+U&qLR4BuW)blW0EdS ze(lG*3KYDJ+07)nj3FCKlCa&SK_J)N#%iTVA_o}NiaW$jk0f@NlkFU;x>7e(Pv zw2OwO$N$xYrRAvx*`|f2&*r97Fy5hXFq7Z(^xSOsx;&Vs`?qiTufl(?XUt~vTRoBJ zSozdCygiAP5jVoIY-VP~*`a}Y1|J4-_wo=LV^n56E&zVf+6*i+$es3o6^5YLwjMSG zrj~vRtpl_*%%Vv7qA2xLA*FBc=fR-IzjYY~6UBzU9|zCJx~y78+> zw8lYEhtmSH{Ww_C%ZP1$@n`ZCqc^MEIx8A6J?=%zBP@hR^O7;!mNo>%f`%{6>!qmx z$6MR?I^2EGrz@a!z{L>C{Nmj7L>dd*SRg2h z3dgdX882I)_vhN%Z%_KSnGF<|lzv06qbPcOZ3LnMJ@d#zdP^j<*lZT^M8#J@DO4emG?MY`dD!ndIzSUDkoQ*a2ss=5tonE zf00FCS*eZ7rX*j-wS?ckJ-F?1E36b~4GAKj)-D?YxL#?hH>}XVLMPG|C!{wx3Da00 z5NKON*-NxMD@0gw3v}lGewG-7rNcjY^Pl}Hg)i@Bk)I=w2n2%CObc#@0YK`hqqY6zhmX}P@guL5#OtZC zBg-PH&^Bcn$petLP z69fatkGVbV50n)GaAk3JP38(vgBz9DISlt_I-XB8K zHv~I7dRIFnHI@{tg-k+{3_`Kkv^30)7bVin`!y^nnBCO-`}a`bkHn!x zwyDidC;*aF5c)SAT;tUJj=nccezAZnBx7XYj3=C$%yn&K5&k1J5$|&D|TX|^2S8F1Md>s0gnk0lk>1nwrcL~C- z4hf@X5g8f~5sbZzX&K9VhC|6%%^s`U&2}eXEMO{@2(O-Uc@!Kc_@5Az;?yK);YWPU zyR>8^4EEhJX)bxZhw9{wLmY$_WbFzl8zNI}U0)vlGRPmp^DwwWRRu|>6vPiId zsSr)d;`eJ_#Wj}rv_$D(BIwMO6jtZuHvc-Pe>QWMANi-oQq-EMt7u!eE3O}3g>gtlq<;~dS%_V0lt3N+i z9)l#5xSe+TWn^h2d+9_8z~%NTRH|?wsGQSLY+D)s4G$uvK$3(`42yU;EMw6LxnWaV z6sNB2`tILPyH;C}UAxvIl}cN#4G%QShESW&^X=s>k(xl1L!(4+HUNnGRQqN0$4aJoWjEUH`cl z%Q|~ohMP-1PYJr%y6<>hl)<$$E$Qqpj(rC{gk^oy(Y;QtFU_y6t`7XJ>;f8UaDN}? zZP4aZk+{B(UUdx?$nVgyr>)D5ot?_NUi*Ci=vrvJ7~49f!_8;TrLxKfiu4`tYT5Rz z7mS@eKz6c2?shvS)=6^LIgi3^xH&H+x2%9|w}Lqk+zwTdBlwUG>LM*KQ&&gP($J70 z93K~D)|D1zXGJ5*1uVv{h2D5zF18|9%R66QUjAtXL;7}1LR?6`)Q$QO65lboYhN;< zh%rulyDEj$F(A)NOHYv$SL8#>6_H<2CdvJ1;t3 zm>@g*bm>`)WqwuK+8G;4W2+Q7n!Kc>WZ-9pJlPqwvBUje5ftAExo)>Z>;owaQf$|n z<#{Q%f{;iGC4#Kf4{n_fWifTsIEkQW*U-eHUNdrNc(k^^VzUS{k%^FL$F*HgSu9d% zZzLL)sLoa?o2Q2dhJSV*ACnU}C7T}jz`cwUOC|&aA!139a?DizU)Q08mMYb4Q7O!# zUE?At5QKCzx4*x?HuvaXN6!!za-MyXC^RTJ_ew*yFylBmChd@tGn__nFO$PW?qK(t zZwdRsmSK6x`Hw#-DXkVS?ANQ+Q7(pJ#c~vVPC`99H-stT)U)po4#;Qxr`p=v*%$)S zDwRfHu>%uuUgW6#;NaNUy=3&E&^HC$D`?l75j7usS_&WeY7KvA03Y{rM~6*;`k025 zJKueevg)&the%snd%;>;qn;08nLzRJ0;Y9N^5BFVwIAbv`8FB}1kHfFIq&1pq}6ee zuhv9`%MCakVML?RsMYTx0lnl8Pol8IwS^fI%34s3js6L}X4kO;ZiW-FG$xvoymzXg zr24_u)b%m3AZBjdz{6! z%UVz_m!GLVU;crKtWId)K6kbHf25uNYZGZ6$FFO(=jwWQ{;*Q1OR0CBUGI)yb95j9 zIhZoz&iyp!5=IG3hf3&mQ_T+%MI_Zh8U%HU?FFsPZlDs%LJ2L$k){cRu<34pF5FjEc|1E{mcwa56q z^#TNR!4u2ha>Yg*&pdqMI1s#MU6?lfuUb)cY@s6-Fr=R~zyPb3F z!HD~^7iyvLQUye{ATS4_u-Uu0V!9d{N#BGxnV;X~GHxhH4b5_hx(HeV9_Wg^+@yQy z&VrWsr^iRUukoe=Y1<1+AbVft67ND_76|tYO@D&;NhmwFMiki z=AU>dKlUJW2$q@xWntM1i?S@s*Jnr4*CFPYZtkYtQOva+6b{hki7^Ql1{%D4u&*6I zclyo`)7P$Q$Ev-uY%2|~c3Fbj9+_pAOWfyjue_i{0=QRY)%antw$T?4z5}TbO!FMi zt;ZgWd!-3&3k|P!$#4)Z>2JZq?lxpsF*a4)K!Q*ZNZ4?=04mq|JelU_hiHgB{lH_; zgx)GOj@$6U^6Pmnu`-+Cc5=&U?t8^eA|j@4LBuF3NLZJH2v7G9pY#+p&pu3}6H*>& z4#A?tx%pGoZ+E%*L^0#8#yU%wiK+f&e8Z5o08L-<0CV!prSIN+z}r<*UT8uKN|WQ( z1@)ydd>5w0w469~Q{IWs7ZdI|(x|lo1%Jb^Q`oRfkd>&5Uep=gi}dD4yvJbveIafv z|4~XDSHdii21Iw3xy6PYe|vp5x5T9v758)>(H%^Es%VhFf?+(R&M$!G^<%8hx2EqM zV4Qb43op?U0|jyfuP94M`;Bxwx08_@uiVX|yHtc>?BQcV0XLJVE1qEbfacf&7J*M! z4{)_pm371-LXj>;ti7>~0XE;!$b)R?Ra{T9vbl6pkWc1x~AUB z;pS>Hg>p%97=-|n&BT{;iAKgf?hV6qey|Hmn2k|qy-x*@2q1y}g!=(9O{kou2}W_k zD4F?qJbm*(6oApp!gghWsz9QK5Ao~M5Y4lN$G33HPs%u<7h9CXxLwf81yi z03H>vggVoo;j+5qxk7WOH!eldYx@jFAxzlJ?>CO@f#Hfph zYoL#R_SL1{V;aublv&h53v-khwGkk;`*0f$mV-z(XY;LwK$;2ZE@yL#N|qcN-4(j4lld@=8RyN5$5U57fpPA=_-LT4j-oZOQg5}L#?dr(j&V#{ zpskM{V4H3%K^7wBiqG*T!nxFY=lRb+KAfID49z4n~9scaAuTOt>?D^|WI;NqljGEUEW>t#0i#Ov z(LN&mWY3v%=Ru5Bly-I0w(9QELz0K?ma(JTL=vL^7e`!u7y2c&wFuYpHk4dDc(@hq zY8X6gCm9C^8$%Bs^@AV=25DA(!d4Q!{=69_d`K31NzGW3CM+$$EM^U>nj-mAGE>l8 zqREt?%Q+>gr`YPZBhZ2YVb6Ykh}D^sUM&%D|BBVJ11oYqb@2?(&;T&vtHdax%G! z5|Kr^&4v+)cA0aj8F=dKQ9jh-_*u=vg2B2KWn!P8Qk}v*+-^)vOyu+Eh9e~1VWO7B zNGx;NY?esFA1(NYX(^%w{-l+q=F}*W?~GD{waLi|)gklw{A%4q6E*E46Iq0ck|j0Q ze#;o`+=#AWL7ZChorhDxqOlm;36@q85Dc9Fnws$g9W7%dhR9NCDgwzgN6MDNR17CD zcx@BbVA*t{NC8vauZ&u95zwscx9x`ENh}PoaE*dMFini|&vIgr=G7gaV9soo(fqz0 zB@&~V&iC{J0snX1(+u4U8=>!FmvxSl*A>A5BOazxwyn#wq`g;$S-!!4R@+V#-h{Vr zD-aOVN~d_cTWiD3{=)t*@- z$vgqXfp1+aj>M`)ibxadv8+(bDYe;>P-xg;{ASBxoh8sr-cBYl4Z^Y2lo6h0%wJb+ z!SW5M3JqYB^ZEtsqTG~~^2|(4snjtHW6Ci8ddn-6eyP&x+Un}<KR5F^7X-dmkpNIT51i`Bq#HuHDjYu zowIVNGF}fzhbV(J>Y)(?)4lBiaq|U@#ZP?5j=8$8(RF){3XJ}U;lPeW;QhTNeiL=ngrJ!!9wyd)Q^c2c$piEP~$d)x9AT1*>wmWu8Beh z{HwD4ezzc8@RuvKE#7oWTWhz|_Bx#A4HjD=hU(D&5$78$KF>>u5?Pd?>Mu5hrn*a7 zh893BLp76CN;~i0@6?npf8$5U^h?!JsV6DqXltuYJB!40>{34{;hWlIWEhF!18&u& zQkYeb6;za3F;@{PCGI`CB>ntt*WEr4k{WDM=o zt8UZkG%mQHZV|Rs%oAngWujZ~L+anw_e<-LWxyz9p|Jh$f803dbJC{`FA`7n72k5Q z=$UiRn)K6bz;PpkbvQ24E>%nxUb~k1H7_fHQG|_mp_ykle(&qVJSdr(V%9WjR>1%O z2t7$eK~#a_B|KifeDu-DqOU!@l*W(bp7W@fZO#u~CXurx2$@_coa&szQ$Uq#vmx~P zK7a7@v3z~U2Q{VHd#SBQkIJ_3o0%o4_Bm?bX08xeH3vml$HdDeTS9;oO-?pa6O`sV z?f%aj=Y2Jo7y8wUZN+t#_|2{VS9d-?ZQD@*ciJW)Lm(}eDkwoH90G$#)wo2;sZvmE zHQFKAlOPupX^K`Bph!hR8!Bp5QK8X+#+cf*T@P?YsHxO7$Vd|^29;enb(1;Ze`I^_ z`?VcAb`mck9eXLaqVVzi@qO>>?|WbCA7e6j<<;G<5r+c(Reg}h;q9yF6qVEKFEEgy zku4eBnTPN7Xtq{vBD7SnT;?|-!eA0l_}$u7<*KqS+RES*lTqe8~V z_sbv7|9E!`52}7}I?nclk2SYV7Ks9kctaL67D2EWnP_2ow6k*?J}9?;XI-$QAu1OT zfj}UKbnxJubp3&xRw9|@P#%tCX&$DKErCkNN8nToizyJX#aC3mgP%t(S@MKoZGTrP zogO2{AJ~osIpjfkZG3lG?~Ir?u8$iTS#8q$DNG_E1Bb9!{H-W(CDzDUpLH%J&5?M)dcoh{WUAa8A8Y!RGS>btfCQwY1OHJNh;NWmRid(7 zgj|}W)Gk@b4n}%??av=e2ZztLmzPG8RV&Kmllw!W`ei~ZO9)g^cCGMan2 z$@jK!%EwP$Udj_D7^Nz-QpJ?Xe*u&`T)jX>k_K}bCi9!`?JNrpEV;*<%_jZkK*pL` zR7)9S>^;khqNwmA%XQ*dHebEZxBi-6wJRnw>Wt!R1%(DObbgJd${i{&ZZb-=uas!r z#dyJA+qSPR)E(T-_x3b87CKr^EW9ja6-rkUK;ppYi;Yk($XE&UVQBbd4Xr9%vPX@zX43LN_uy3vuX_m~KpB zLx$CMkm(+_h2;th-Y)ZI6{q|9nbw77F~s?a*jZ?xt=UDICW8|a4t@n14SE@Gk1Z@u z<_whFwVirXd3x#|>MOEVDQQ#~ga)>(6%j;vljC2e~ zq2zKmAFn|8>JBg$C4)va%*wDJ*35#lqNV|DuAg2@C!91!lLp#x?Dxd^IqMXW1%SzA zkR;ygV=hj@A^pWqJ@;l6nKfBd1p`#&=t*EH2HZ;|Q(r8yHoh5mgbw!TxIIcJz z`^4jC+AW^`P@S9$cQ;Hd^+9k6vi0S6#fRgu>mLDxJh@1a=3KSom{vv$<=TZMT}wkj z`#Er5moLD4H1Q7fA&s+gAiGgjRU)F)Q3!_d;i-3$anB!d*Q|Ejg!NN!Tm;9xkw zTn-6@hx)XH*dA0T1$oDsRd16{!?2DV&iQ@dcPmFMeNmV5q+@8mg*U2t*WT z#z#C}KlA?)ztwhZ0000mjRTWwA>k6&4kU|Nk`R&7l&kYHMnVOx)0SZ_-!kYQVoU|MEMGi_5l zYE3dKF*;#KFk3}2YD+LHGCL|UIf7G6FgHJNPB3pxFk(nBD=|7RH9S{9EH5=YEi*h> zLN8ZAFN0G}e^X23xuiTuTHmB=_}|mxxT8r|YduL?@Y~7qwSD}{p60uyCM`DaxQ4}g zVLwY+Z1&#LZAd6@N-9KBW%=UP?Y4yQ*~b3OpH@C9M^!l zUvu=^&MY%Nj$m0cJxc%NZsMqMfLJ$7Mle@qeD}qc?#{g6wxN1cGM08(>au`gNHnH; zaMqP+@YTbZYhs68Jk7R@eo;tUYka}k$_eV z%%YIek7lxnV$!FS=f0|-b!+Lvuz6ArU!SM4hI+h?WVnuk?8~*Rfn0=gT9s=iVK_1J z*U8SDY{r|4z?6iWZ)fkbdx>5hUv7lSteSnw(Ac7KoQiGf(ZP^!QoMFskzzf&p^5I* zzHvH(^&lMnyyK*{s8)l)8ph zR!%ng&7{=0sO-|mt$R-F*2AZ2O=5D1gl0^KTT*#xRK0|I%b<9vk#66sezucwzMpx< zk6PWrvvp2Lpmi_T!Ira|d`L}MyO@vv`|0A%pwp*)R8v@JMk;7NHp8ogsf>B7fH|OX zM`d=5K07V_R8aXK!>!J)>z|>By;LOiSp!mEE+BRFj)-V_oR_{BE?n z^3-hq_R#+4grJ_C)3Iq>i<157o&WN<;lOE6e}{&Qj;eD!zqYa5&Bl||*=>A|((v@! zzl5>H%Pm3v5&!@IGIUZ-Qvf43`3e;Q0RaMSEaibh@>rO@NYbt7S@O=@X#RcjufdJE z&ihxT!kOcY^KhipT1|#x@PQCQy6>Mm2Bm$mk!6qc5`&LuGZ0oZ{Mg8`IcFL_P%8{!)F4nsjyfDf zPxJ+0=oIW5PSgKgw5gCc8)fWo7B*JRrk`Ol&24SW|D*t6*<{`52VURI)@FXiZY})G z2NeWf`H2BiZxyqfmY*CDdWBj1BX>|Vv(mtref49f#4K1K%O=Kn!!ga5FprnAGxHI6 zx+#I2^Dm(?z+!_&uv1J4hXxrT(~liaOfq76DPiAozl^rAMiPQ$r!{)nx3J?ya3D>C zWWt#MD7pz+m%fuEIijY)x=q!t91hb8Zk2YcIiD zQp80_9G8}HMV_QyI=1YjVUTzcqb;RYMvUVmR<7{unt5W0wlxP01Izq`GGgjIV+pe| z7mLNxJS$c3q|5|bVOJNRVel0fXv9kZ;gq2=VlgoWDZ!^fChQY(QS6$VaNs5dV#*p} zUWL?6i=s$Wf|VdGUd>J9uZQG7cIP)ZDb z#N`*`@wf<MAb!W1yC9BbWR8v7jtVGA|eFxjf^AV3Hy)K~<2X0Kxo5V>P$QvXY;x#ILT;&#$jY zaq#RAYl%9aDeAkBmQi?Jn1k#MI${L_lQ#2gR!eBglM3Quk?KL|`{oM$T5V8=O=+Q@y^s`>WY(-d$hC z{h*cu9917>p!z&yMok+Ba?6;{q!fkGG6G0sAVr{!lHMVa4gOI{K*wMup-_0=eE*-f z56vbNhac4PiKFW099oJw2*flu>DHlNmKCL)5D>~&n3U40H6~4T)Vp8ATl1ORZLaal z_P-k24^1cz5!4dfpVmdMICh)_c`EV|jH!cISmfCcIfm(AfbWz~swD&tF+yU*v4bSU z2dK4dqtQsh$1hMEaiW$h#!PRU1U29(%qdBP1GMsM7S3MB!khXGYL^BQF+Zpr9%<}Q z6GDJm%O;Z%0^+~#o9A&qCqHQ$sGoMWe+&%BBZ0FcvEt%oL1mCiY5CA50rof+^B?{W zBt|jYU?ErzTq=`|WV4YZpxAESB?!=7wM0D+!XiBgllC<2%#s2I4J_rf#+l1?N#ub9 zuK4}pVd8KX*IpZKqKIsN){4M^$!7Ny00AT(?)F8$#BMA+L!o1`bw8wL2mJzx<@LV+|MF8AYpFxie?3(+%Q^m}~S z@DsD+?nQt^RLR+s^H&POZM%!6>S-3&qQK!abhGPQHr6_<&RVTTSMvk}VUdkQfW*V) zQQQwEd#%r8p+9kzgBA6Hm^6vL$0J%PblOEq$P{4qBGUPZyN=?T?|A?8DC8*x=tV>K zzvdTfK3}wu(m;5XEC~SdF(GnTua@0QkA}^l1|KFj&ViI$FJ0S;%=6_o2a*#RArDFI z^?t>!a1GAtEEEcQg7-dp-N5`cw@&_qt1DUyf{f5Wyt~{NW4ul+2Ry=$`gK)m!o{J= zPH$_5(G$DsUY(h$3&L#{$~#xVB+GpV!Jx;pa~k&1!{;xv8!-6j3y=tn?H&XH@#gY8 zF3Nhf9A)Glo5W*h)&}v&ziLfGZ>GFTt`2=~k16cJ)urh(4~d;Knhp#V%oQ3i(1A>f z>3|^JBl+;qJWm|D^lFKo*``8INsPtevEJ(ctD}p^vJN0ftLQjvRpPQ)_)^^U`2zPL z6@({r?4ySXrhU;@^MwQ9vchzZMK*f~h`ar`q~DtvyerqQX2weiin8{AAqM4fb&&4) zfk4RTl=k8wowX~P+4a%)p&czHN&z3e=s?dp2&jv2h34{I4n#Dw&_BURB%pt~^%*e& z2+B5Dkx)UjOHRcxw+J>uUH5IFo>z_bUe{}9;N*{6Bs`#(^zzEQ1q~ER!hws@)i#G# zMkvHmT^=P)SlyPyXduQ*2{cH0TF#X)Ase@!v~}MWi|W3mPB|Gu;vlGj0KEhSSYC2F z<}GG4hy%B~Th>*Xmq{smFmXIIKkoP5d#scgD9joNm9gC=WG8hI^~ch5S}!-=6g1avewxYIVQre|(qAj2kT z%#_gDhW74|yCC4|rIg1Ua(~2jqgvkp5}sh_^ycjTI81OXFTHlmwO_%qvIraw5{Ld1 z--Rxtyr~?~Ue#sa{f=2y8|bZ+X?C~xhTSI6isKOoeK6U^Usch6cyrpSj1~+Cf}?bH z0NJtVhP*J(%{$g<+7~zr?Z@L$U*;dWalWp2RiB+~G%pW%+`6|hOAu%n2=(6}Z4Ox- zHF_y9kw4-m$7eSoIB!p%AU_P9mhPj=ORMXP%QSGyvX?}AYB^+G`q3i-zMN%a2IV<>iZLZxVc zlm$QwhK|mBA{tD0Dy<+m(?Q1XX(XvSi)>5>=@)cELrMg+h?d_{6ZFzRejx@B$d@ zR6ciGuh(;tzr|1PN5P<#i1z!z;AuE~+e(OhQOLLE4NwWtiDXrBwVzC%&9;;JDu#gD z$kS7Np(_!|W<2rn-QP#!po^OTr?l;v#z6(4D3BD9%)cdh!#-0GR-=9mEOr|Nn83Zp zdE(=nU*Gg45eymtQKDOdQ*T={IIKcgfKp^WR9vlxB&Z?%lbQ?%o7J$*v@Eqiz=E@H zuM(HPLcICypHDFu;o!L^m7yxHqU%Hq+27d9W;ia~H4^xtA6AOvl>*=Az@ zoyy_OZ#z#g7~&9$iv7G9nd4v@&wLi6|6}I-Ueid|IG&a?n$u8v(Tl>_l2L7 zH8^ml4mI8sl+}x)o}m;JCb6>SNU%SqIzz0B&33T*NiJqFH$zKp@`w1W!#Mr{CaKWVr zBFdOdHYek>!7@3mN==p*Ud%>{8DgILVIy~h4a1ER0DAoNXap=*~BFIUcw zzxG#2m#heD)M)||HWOYpQ`EPgAc8Uohp#ZxTo1?@W0P;PZy&SDN8E5m^fWD-2%_B+ z4)lKqAjaOp=!jkt(!~8W)9w}XDWdTC_Nqg61_A*!H*}{32wXX~ve~%?h#r@Xg9@S# zKzy^Z<=@14hhEjS@S5#&5E15y!Wgy*ENs>y{ zg24eCe8E{2#8zq<7L2l6i}NaoRQA8_T_8I8n_XQyqe(*vUjzEqiumPiU{mc9;u$Iv z!ys%L91J*yX=DZpNSDh8PAP~2FY-LVklgSOu8#}q2KM4o3MSb%FILqyVr)E>T{C37)475VzCyLo*m? zR~zjVDjLgsLFoE=2BJv=rR(c!jlTc8m*3bo6+MJ10m_n(nf5Hk9Cd22Sc7OeTq4JD zi3EIcJf9G0gD?0g`(|%jDT6>+X<#k8GNK)cLk*}cAKZ$B?%1i9l$sFkw4bunGT+x< zb6pcob5w)HX~yAbE(i!jE+GgQ2K*LRY$Ch$8HmXR8N^aH>vm@p5c)cZ)_eMg0nz$A zs1^JpLoY2EB_SeUjM^0Ih%pS~+@%O-G^*tiv!YquED4|ofh`h5QOI!|50%Ht7~igZ zCWwVbL^K1@>b&bPcZM3y&~-X(93Og5m1RMMiUI2V)b)(Z9F6X>0&>Hy@0s9w&>EcM z6SvQYY5{d`GgF$%NL?Ty$iW+2`Yqz$}FD|`k9FQOolh8%X zp<5P1$KYH>bn&GY3>g*X~#DFl!}YUZ0E1 zm5>Tl5JgajSdw<6>U!+<)y1);<*i217{gt}6ByG2mQ%;-S=rYVrKs(nMrXo>XD&2U2Q{ypaT!Rn21L*o_Y_MwA`}p52u=7lk>dnuj=ePh0$s*Z z>L>y+i=07L?UWWo`~PM4Ak>J^cTQsPB&9eb0wsg`=Z(>u;nFjC%+ioB!;knH5@CFX zxQAc`4k|{34#ndU0Kthocm=sdyzhTK1|-zoTOWzuz{C5`Db9$b!xVLQVtCkim91~o`OJ*d>132G z3DWAU3&P-z69k*fnY^&b>&dCO(UkDU?n6a$v2>EYqw(dS_UshzT2_5BD^E**?RmJpHl=W;yHSJ7sr zz)NC6s1z$12n$gX?ax=`&1`(=hiis>>E`S}~(`(Ub=1FU$=g?GqN*6L^}Mw_F#1O-tg#*ztqW|EnVhCk%J;3 zh@w;#1c->VTtF%b7{ng?^NZ?zMWvepcwr0mtp>z?W#dbOXwAI;oc#7f^4XJr%-nf2 zYaE`)v+fQy?qS}Pcn%T)i~tZ|{@n2@l~mwFDf-ajuY9vx?u@TJp-o?8qc|<8~Ww)H&c+pMyL=dC7w~1U?=L#pRZQ<0a-}#=lhWEoEn* ze(LlIEr|9g2D&R7?e3km+L!sOJZHqg8Oh}JJC8;#U~iQ7RfGf}5(zfV79kSA3b1yN zjzSTyCj-5N$e6ALZYdoVa>kqr;;06M?u%=@%SR2IwWA5#?IDWW$=Z|I3nL@rP(W{m z>GqN!tGpx$Xx=IoBc2L!ha~b6gr*@B-$ebT=(NQ)1IuG-&G=J(n4LO(Tr+a@@G`)` z*0EE58rM#uQcF(Vm;?})?+lMl2g<(of`C%N3l*>d!FnSe0e#0q_pno?t9Y}9E5}T> zcYgT_E^@}4d=N%aL&p!(JNF922{|Ib8JT4A=X;k&Mvx?o#!LI81kG~-$4BtgnU3#( zHBeItZV@3q6KE8RxSTfU4}mP+$zNQ4=c|J_(t8jGf5aIa5pFM$24_4)f&fX}Ga6^Y zY};c`8tp{vgz(h4Sw#g@K<&1OG86TdLOO1+nr!RgtLS5t3oFa(^3iD7@_zu)#V&_u%w_`WC=a3nEr2ATpz?&5h8fM~eCS_B zH$+mOO*?AQV@H9$oCJc*rw%waT6JhzufLrYPR5`f@oBnhYy2_EMr zYAHDoMsCyV4Z#m#N!(goSa>tNE>rzlLFoQYq1{TT8AKRrh9ZcOkqfg%AtOXCBJ+;QcZ?6v? zJB1@6M4*V+PW^iMB3>gSkA`Qaf7;&Oo~rIDnfXS7x_RiNxKhMZi+ z49P!ahQblPWSp1@s*lU4iqL;m&Nw=R2aO1UB4TPOwXw0hw6c=g_;B~i)OM|wOjbf6 zxAt04qrCu0;0;TGgC<)*XBsq(^pW8pF4GDKC>+cWnYDN8YTURL5CdNsh?91-IYkpV zBJN(kFu$_#<39jF60O?ymEQv8c&OnAU#XU_lWSyS5SgsmMrG<@P&qlcFQy6Ue{R{oE8Ad^)QLfSqcRn-b<75ePPDQGOa z#4{6HUQ}QVV+yeqtVZ^(iE+7DX$Tn40x4{ki%OaRU9W=kV5-E*Lxm-5MT&${LG~dl zDvM=B)uaL}{9ySZ*@U9*Io~(qTgGAIB`On$f`W|vU!1Lw5pwUR3EvYoJ zO}n}o%)Ch;Hj2=3wPR*L>)E4U`XCA2nHZ#ptNW~az-=7GboLQ6n&?Tl^Qw&C3 zP9;Cg=A&1Fvk(@+)F__SK=Tk3?m80xK*9L(KhC3N7StoNlV24r8T1GYj)5USgDA2R5g4ZWvDZybcNSU;ND@1;V)u3#u( za=PODX~}2TZcIErR<;-Zg!NN+w{8`4qlTAVv)(`Iu~HCMXYXr+&~t~r(2<}JhT~3C z5k-@F*e?+`MaAG#n_T6LhmnQ}%EHL}gZRmVR1Dj$JUI zQrz;3p{S`8g@U8oY4VDbHp!^HS=k{KLc;8^pAhM2X0>vo77(D7O-A`F1u^x_5{(!d z8=+tHU&f%&ojF}VQ6>cN3ZRxY))&y6V3@-Y5=yW*j8-h58{a4p;44!OU)V*&^6Kmz zXu>TeFu3HMJN@d{$1dPfVDp*7qMxAXF6lr}ZbSSz8!LtFnjv9lSy>4dN7#2lPz&g) z7>OG2g0Dgoh(y!8RCr5^gLP3H8rQ)XOA0Cfm2`R7u5aax5vg^=O zMJ@}g$VN$yt!Cd<%Zskr1=hAVIvt<56S* zt!Y~ucCT?E=EJ>q+A3j%U~v>I_g%3hH|XN$>l^FWsFfZ``a1y;Pfs2Az0!VTa6G*I zc&NVug&WI9bML^ZfpamDvM?cyZt&<)zZbVH%K>v0x%8=WDYL#1&5czNiH4PPtKZCr z!0a4VK(q`czq$S7k58U_mZrmqsg|CxWO-2ZLr|O?IMdq+Vc`{27UaRQfxA*)DVG5j zLxSeUubvrXBg)z>LdwU6NzpJx(+83k%fi-ubA9>z=QH2`KtrSDv3^NVKp)l#V$pjp zC<;_B@EOLkQOtlPaw(~DiT)}^7lO&9Kd;wYtr!Ge2eV#0A+c8MJx$K&Y)eva$#r~? z=_5~m-}gNSx@Nn^v@ADy42F|~hy6S+_=BN%6w1a48`*wot|pg(g1=(V@x|PrKh3%R z*HZ|P2=64zI!MuQnh=D3e*ca>1w_XT=N`I0;x5k(j&r+-y`m_Zdv68twj~IRAb*Z- zP4;Nqu0Y+TxiGYP;3u0-sYuZKf2ykGigdIl5Fimon?%M_CKmzWp4q=!0Wr7#XPi|n zF`M^~Uwe3atkYmHs9|yGrpV*PGyGn%y-Kdk4eGnmT{;RA3ZI%Abojn3h?QO2RfDpE zL|pONWU7@EL~>6@zK`a{?)~!`%_yh8KZQl*(<1|?nkfu}p>r@su8)Y3Lms|LPeo4@ zW3f=bW3|~X3S|FK!_wP)wKfbhs}~Rq-ZkX^N>a7bW$J1fvJCCnp3MRX__d=Wrv-xJ zroa7zQek@t3}kWgmLDR59{X8xB+%|qt%S9h8_&FamGv&o{ld)Z1O$EYjlwWUM1W{z z6hR!aEFRo}ATpW0Z0jcrn&Sy>zN2++=Eon(U>J0RUY_?)+KL-8i%-C=yn9Lgjz_jh zd&$0;QV;Bl>N>{PH4#AR^gaA1-Qg z*f!HKhgv!}^W-xEqwDykUXY9dFE2i@(F1`Mu2QKZa^X8>T(`Sa&5Bbm%xHj>@`^)@XK`o-3L8MUx{{^JvBy(Z58b?}xT z(4&DBYH2nq8DYiU+%zJrlI^LN-?FY8M3sy4DFkNu)6@Tl8QHLDxXH)Tq7gT_Tt8hS zAfCSfg@{D*TXUKmEDN8g1s0#ohu=<3rAd!6;pH#d*rjS|4u?HT-H;R~;R*O`(j$J; zZ!7)}t6yeVK(Xwxr`kZghb_2bu*aPM;`|T=5y?g(kw3L+ON1r24>LmzicD+k^r=hd zC!k50xJiu!TQRW%5Xy0TS#=VQfXv#U8Q)xASO4mIsbmGiyh1B#C9YI8K zClr9t|MU6Fml1rjJ7$vF5s};8m(3!G48oY(Ix}(s3TW?O&%k&5UV5&h0)(ti@QhNJ z60uvutnHbff9I{T+$c>AX35>JR7b0mXsGcr*tgevM-jx?0*E6?J(tT94w3vvxzed+ z35Z3I(@0-N35s2iLD+~v;V=w{AxQ&-M^;0`E>X4g1gu7hh1>v7rPY8kj7GYs@lXv! z&70TX6^7gFEVU8w5Q6A|t~<9QO9JBH%%Ua-4(0R(g2-e*Q8ORyS;R#k;xSKogXFYm zB?zAi0={z4fWV`)m>Z32>s|$-62&stG1kO^Y2;JT5VK*l5#!@3h{wA~NaXh{=(RJ0 zsz~2lW)2iJGWWqeT^K|XoiXoI4G<1V?ZZGMKu66E2nZvH#M20dO?IRhK-}%x~|j0bX8hs4?@{b(hYB~ee` zb}0!>5H(+s-0(ghayS$aQC!!gAUKQOz0Iwi766B{SdfGAWOpew*3IdUeW5elA~UK= z5Y{3HJD4qn1Bisr_XurvLv_zra+7*lNW4Z6IKRHGf{1%f1cV#kDtVS6vH0(-3L?LC zo(2Sf;0_@O;<2T2XBjSn0J*T@H4}Jci7bVEcSWI*Sr$amMHF48Mo82;;Hqaf2>#LQ zYHGxTPA7oqaqIQf5GYH4@P?B!eQH3o?xBl>1RThOCJ1Q1*&+zZs9tImmcp_e+b|~wI1`{>5 zq!SYD7n6QzOxtY6besJv_B`jla8*ndCwr|Lh_$`H`@Em$Jjbt2d1ziD@Iz-pr%XW5 zIKpbhUUpOq8ymWwlJ6K0&g~|`WOU-;R!B9@n+)8gGEAYEB09isb=z$#i3-8|EY>!Z znhslDzb5>JIz`&1q3I_Y@bR-e2nex-AM5rV?K9e<{n*xn$n0zA8F`A`rzt8Ngy{u; z>c!%KLAW8HVo1aW9-^;>psvIjM}$Bk1m(I4X1ZNz5C_qaYFIrG$zh&MI*mXWy1Nkw zinaoXiDjk0NC=<3wkQ$Yn+$ppr01Ekt?S)wqW{+6P{N=ouyU*L@Jr#FldX;a=UH_-qo^E zC}$#ix^4wMUoZ%R*X(3RoZ(pm?ouGU2KOj60I$YLxu{gFj@bTAQ6x$U#J)gO3qcSq zip?R7ZIetR2{xO~8>}1%3vR{MayF5tfNz(T68#RqYgg_f*km%zpCnyfyUM6d?JMw7a<(F_}GVwV5}6fP|}J*ODF zHP9A)XEEk1VKBA#{@OF<3C5s%NM$G*L_E$a#&W<|U_y@ zN1jE-60e6t^_JBOP8F2fsc84~q0pzy6mMfH6-PnE&Topc91KW4+*3m~o2>vt&0@ck zaP9y#Fspj#YTL(RI8nN=%YfKN;J~YvsIHTu*A|1Mj+Awht|4UHk(>9bJ!z&x!os%R zYFS@d)QoMuq=Zn|p)T``oWh#HV^A@g!8r{8aXvQfN0$?EC`3m|g-n4SiEDax)K&NO zrEp^Ji6HxHLV{l)s%bePBdIhE=N`!0Dxr@Ifk3fk^pY#cH2+u<+|9F5Hvdv+LzN}W z#4uFqet|FoX$D(xP;t`^-8=|)_xUfIaRkCAHiw?>~`|k=M%3jkT zzO8AK%M|F6$vWHWBt(ToPOX8vS0N*j_M{ly00&yMhg!W-TYSyQidMNxBD7j)JiImU z`f}bBQwc6cGh!SEC|$p!0fgT_J8Sb!>tOX$e59oVvH0pqXn#)IT~#$95mnYy9uY~z zSe=LyGBNC8;XBB~n0}21M?&M($D@1*BQ{!Q!r;glYD<^I%-kpOThcCm|sPxPE zP&W1E%^P3R5y}VN=aw>dwv^4=2`1eFff)15$6_1=JP+9mAY!wAOab9Qrqew|W#f!M zg#Ginx}6{nNVM zaW&Rj8YE@?7Iw(aX@e=Y2vK$xI%u=K8+Ik@B=j?(NtOfiZR_oVUa1vE8Mq*WJJsi@ zcTZq0vJvKIJ5LV{Ibl`L8xAw7fh>f{6pg1{VU|O9lP^t`4Whd7nRj;EIIss-RYolQSNptLN4vHSg6xqrS$8&m*$o zX@9Y}8iBBZW*FV3m|yLpISwfgpJ|l?LM#zuDR?xjoH-7XMKlFIMw*B+MS>ZHtuY$I zuC&@prx_A6oH5krkZvS64wE6v+_oAG`i|GZdqy4yJmk0$rUkH{HY5-x_kU(nsrV2U z31`&pE?85ZtRG_tTWMy5VVNK*8;&u2~zz&NlYX=->bO z-+zC4`SSDADQqMRv6%Ogdevja9IOaQu`X2x2x&4m*(@a_$9*Hi+C-BWgIHL%NXuN_ zkhz#4&>H8^W-zM`;s$d%Z<(><_G7JnIg!r?e36JRnSbk>(<}8$keG292rPJZ-HGHw z?J{x`aj^Lraqy4-_Mz{GzAJ-QuMS@M@agsV3u80}&F{fpH}TaCy?ex_u!j z6*pP#+*2+!SAr+)9h)mpH;9U=7DbSm5TZG4TcJ}~C=ie&6|&?WKokiYkl6^JNWzg! ze8g}Tp4p2MG2GL0d(shVKP8oUrkKbXSWNx);fE_%&z$@I;>Dl(&;C5v_uN-0_n6$I4>EMqs6ACt|FW zu^{|YpZf;SUbuM^I9vt}=)x#IXH%}UmE}OTi4O&$d1k{mf@C7#=zV<8Zco%oQA`oJ zyX-r9a1i)awYW}B(Xj0w@9MR!xky=y7*dgj-8s@-I__4pg|J>Q1k$z;MmC;2iS`Q}=$RPOc zMoi2vNvB2%&h1&hZRC~E8QER76L7__~?`W+Nv1cBOrL~D=<+e@%Y%M(` zdEM6=YPU>|jr2SUog(TKLD=YrNQ7Va4PJcp>JwNs#7nCw=crR2tf%14OB>N1+B6+8&iTpy9R>dB{l?#7j(0i3;iqZi+#?SnK&_t zwc8#+@P%RKGvDX?Jm)#*Ocp}>uJL7pnCJx(4YYUub+}Rbr62@bmnEXw`rG&J-L4f2 zR#>a0U=(O>fzE&8x6TZN_FwO1tSYri{nU?+ClCn4?OV>5xJLT<(UxXRdvgRTGOIusQ ztH;~hFVNRt?!8@fhbhRU2Rb03TRZf595UJR(_6Q2PDC7j)mZF*{(1I3&p|#UzyL#G z(fn{vEcqs_m&ztrc!iM7zoEk(LZ+M;IQ9dQg;lEb%&WT0A0;>J7J;d*Y^tItnx-k3 zc^({4)2_gZ*EfQi@Tl}!o1EOxsi7qO8T~x`q_NB*4qRa1bTK%Xr_kTm*QXe3?=07m z{o?3orowZC%;BPdhm2S<{bxGtQEt1&o5j~xMCsjl)kR-FP%Zx3A5RYs!Y=pXvTx2K zBhag&J!>kp)v02KZswmXhnJr;hUYv7P7v~8xc?6ycnmO%=8jtKJN&-7=O%8ZTwqgP zI_v`yvW1BN?Io7p&{T7!^x7ak6BGOgi8_B!Z$=Khc-J|>58sT+`tIE;tJ7+I1=Dvl zenK2(!5`kU8H42@AMEc_*b*Q5xS>!e44Q5!Ufcf$T}e@03+?uL0^=?ZBB5NS0y5=` z3`MoniLbdBeDhjM0@B(C?SG$SQU`9LU?{yn;^j||xU&gFJ>=MQqj|) zF&m~vvL}(9gOvghN||pG+~Jq}`S^ziX0!ebVR$}^1;KKF(po`Alkxa^LO^1ytF~bM z?*AY7U_!OzP8%5dG71hyz&$4;7M8<{i(#s&t*$~^`roSY>(@(Q5Z8C#?`pSL43@F> zjt89;q8nm_!D5W+m~rEUIe%e~$3J{`uIa}dW@m+hkPr5e2t+o% z9^1W;gt=RgKc!y_0_Tsaacr2x98@x1t{WX$<6$5)9r?Sz1DNAg z&)GemH6sex+NPdmQ<2C{@L-t-p`06Ws?=*tJk6YLsBk&)>JN=s%X8r87<^I<7t^-0MuBa4C%7TE(xUPz|33k9`E}*yfnvMf4HKJ=xH6)N2`t5vhV^I$c z5B8zF50@PMc*sN$h^KD;{Q$l-waR)F&!!nB<+MKAfCy39G<~S91`2keAmm68bkezY#4v|eRl+40&(b6=~8iI4;=hRg# z$}SJ(l4;-7{5eTaOw#mS2HQaGH41n`!w>BeTBCV8<9nISlgQ!8`}nD>v8K$oQ|{t(5%(I2<7kht@SL zhfQY=p1Hx?kGxi!2O$&$fQZC*VsmpzTkI&#aOH$W)6FN)V!sqOQi1(}2_IggxYI&q zOrd$gH;pZ#a2LSkVnEb5z3jMzf>;{Zqpn`iO}qkfCD6|HZTRRqjO8|lDYAqisMv^{;*YKK&Oe@t(`Xf&7L(6_5Q zN#V%I&8@gLu^zx4CTC7@^0ZE24y3`NN5axSeykNl1}6w|*LIQ(0tlryW(zVXiAG6e z66KC^Nm{F1+olD=%;8=|(9^3=}2oyA+p)5LN!=OR*8!_g~e~6?L13 z3pvP#3OPX1aN@Euc^-uFL8nru5QjB3ES~8&4y4hjM`>qRF9b2frVI=s9gIQFQLe{q zJL!08E0){cJvVojYdqZzAEWO%SwUXk+CcB&&CTJv6_^7}Q-bA9fgA z#FLP#tt<$Mw6k4-u3DeRfE_|$ln@y?CoU^v=f4q~h(i`}7-7Rgo#Qx=WFLUg>ib4P z6OZ}G800`iZ3zwpL7XHKsdP4*O`jwaw(rH43t#q;Hqmd;c4ki)o+1QBW*`JE?FOer z99(;uLJ-x(8;&a5sDk7{OqkqPtbc()=-G8Bd9fh4x+gki1rWhtZr2O>LcNJOV0$>U ze$0{N1qhJBkb(tKC*&j| zJmF?6h&O0&GYFyvf*&|MnRiod;<9GR733|nTTodc5am6hr(H3cPnx_S@*GGG#78S| zKo$YBt4S0Plc(_ebVQd^kiszpsizM>AQJd9hgm^H6FX3aoo$Xd2OHT0uQR0}VX{&h*lL*AfL%^YsXGcdKS3nq^!e5YM z$jpLx4J$~4#SRcm2m(O}k}GT9pK2I%cXZzB*C1{q zhI8m;a2YKICPCz4(T@<5SxGHX;VA0qzHuERS~Ury%xi3947!GfA+?EdNSA9sSP1QL z7{LSa-~4L`q(B&+9TKxp;$U)kEE5lYFxji&h=@aA`(z)}fftDC<#s-9?l^ybe$KLg zCc#8oYiC!FhJg?PWsa8cOF~?q7E(^v1s>1>LV#HuO)XI&4(U7ET3TDRYBlKU>eejF zfw*uP8l?AM+iemQh$)N{AOxKhD8U2qpT9z)1>q#d0lcOJ2mX~NlhZ!-;0K3C)Z8Ir z7?=+ytGn#(nVA3(S@L?9_`$O0wZ`i2Vl;f(fn<&q9<9%+U91{6Wr-x|#ve znGt*j|2LJ|VbpVto4>aYjg#MbY)IV97$$_ZT3?j{@nYmDSj2nB85SCq228m z8n=V&*hun9aDbc>r<&6)YAkAoq-dSk^o_(Qbne_L5^Xv4vkeePWCcR zxKRhhOfs1rd3;!z{{_?Gbv2ljztH6NScdFwtBo&i0T2$hP%eXA_tg-~>7V_+cVT}FLPfYA2E zjZz>Ysf4~&J;!iZqzgY%$N0Du2SFyDk4`2FlMfFMf7}loJez!Usf0nKhsW)(+G!I{ z2yARvDE9#eu^mF;nwjK+ZDWR4iA%JD-$HTrWjyT@Um9B}t^;o(F>FQQM1n}IIqhPi zD{BMyfE>#iO-Bp}Rz^}mwdqUkw=A1?$hYk9*p)coF9Rb+|COEDKX`gjnV%V{ zsiYDIT$+K*+Ad_TR!bgI?+W;fGqLm?OUZ6)nVMa=J(nM(-LHac_+EZvI9&6ukw}bS zfd6ecy@)?jM!EQngdnA=66uzxSdlsYctKLZFgl^RgXd8&~20U$;De zF`AuuHvjD4;PL1qg-XhyaJo%)+B3!z!up&oY@cLVcC*?QTB;g`0L3gf_l6$KLkK-E z8!;<7tJTlUAv1r4D#792@NA3vZE zv7Od>LXdjI=;-9bA93tpF)5cE38&lPcAIz)j6KFF2*g6IIp(c9Gn?Y(F5Pp*0shcS zvlU8nTw(2H`eEt(&b@--^qUaHIDns_zH4x82!IRsv8%lbrzJaG2y8 z0U|LnIfLn7QO7_cx3tGe+f5`e1wxd;Fji}in)23P^|o^RO`h4}54|*7zJ-Obj}J=C z%YK9>OKSjpQICTL$+glryMi>Dt~$BY-eGY~K#GLZZNa}kNT$MTVA~k2qk{@_2qm1f z$6*r7)-uCT17cv!b$sk!_g-TaLJ!4VsHeBRN$*bAW?-FybxQ(WU zL50%sG@e-UnM0v)I*L$Vx2{$&A~a3G2cfue)dq>^P&%B0K{#D?W>>Vs|L;uRBbY8ccahl5F%Kgw~@45XlX z!!@Tdx_g40d6 zvb~Qff!&_n?Q~c_8wjg(NhkrvVRt~55hy4LVT{byi^Npi6{Z@&5RUtirr*%5)j=Gs z!%K9TjcTZJjC~b*$bt*8M@VdIXgKVeIeFD((Qe?tXA>SGiOwyD}kpqbpWqL#P!N z000-dNklwKHZ9stoo9&5U>okiLC+7>kss-?f;uq9_!fYzy7=*XancAVLDTH+ewN)-+2PXE zXE=>e8eFDcWhU_2QL8VseV;H8XjuV-Im6t4JN*H^BNQ=udr)wV96*`EjNmB&x<(I@ zAu4e^K-i)j41V2xQ0p1nYBg}N9D|{70uZY?_DqHIO2@Gmb=4V=57JE3FOQ5H?-C^`{j;EFh4G z>Hl$dbs=$NSvbZdH7mH1hz4Dlm(A0{vd~RRkuK;i1Sbz|$IuPY?k8hKQPPk!)1dSZ zK1kF+#~6tHLnQ9QK4e~cG6`YHJcK}x$_Q%)eGrmK$A>%!j)>-c&%L*9)vc;i-MxD! zj+w+vJYSvro$s7`&aFRA9yYh@N5o{%pH25Zp+!>Pe8 zG&J>e#tjJCSwWHTMn6tMo{`7&!GeV^zk3r~v@?lsN$s@t0!ot9V5@S=bppira>CjZ z;45g$hvP@5N6WNc*K|oO{?2aL10e3OAS+&@c zc{%8gg8>C`hv4X2V+;euf%g>ny%dNgR56M!(SLQ-5j}nLNh33o_y=#Yk{SZyH!xVt zK0Waz6&M26F$o$VAY!Zfeb@cutb`@_?>~|bYCcwgKWKtPumK!++mU}5fVh`Hm6F9J z`roT|zws3;CsM{mw~h7Zwp>2~9dSCI(hMYsHjCXqq(BggAbrXptaTus@YHvHw=NEB z4c&NKdtZo1^uC&${7Id!;}ZQ>pQIf@Lr$CG&xjRR<2s7V<=Mki>JrP^;=IgCzxhRu z!{KcA`7&FZS*clfk5D*}wbR2H+m(RtkO7w<~ zLoFD3dQ2Ru<+VMs*2>lWNRJ(iAoB_TtI2!Q_}(DrUA#cEs^;uf{JdFD<68o^8uf^# z&g1b@hC~W-70{>2`y7Vx!{xU2b|$2B9C(dTE)Nd&=<|3(hD^lPjkhekMuzq`!*tz+ zf7UB|<^2eT1h~Y%x72JMmjGSq2rW6T{d9H|?>0Rn)s~qTq;p;OK`sy^!0zKzYQ;bS z){5HEt_vw0hXo$7KPvvGbWnA_%GN&ekYPp0Knfd9S_2G9rM|wt5?kw)as;7?-pR?W zD+Nz2m+1fQhhoxB5t&76r?Ant-;lDraG@3A4$;C*JKyz@c-3`0}$oy z8G^%Tbtg=Wf@i!_*;uQN+6jWrA5fm$uY|)ChCaFo5><>ZVQ>62?N(~wF0ra6;tpA9 zJ(-g%t%vmkM^iVr#Q4eS3Gs^)_%ML@I@{U8g%rhszo(NB#9+?{@}T!C+%US<_k(U5 zqo>Sp&|x4SduZT*y>V|^n>S*CNCiF?IQnm;AxpHaf?D%hl=>00MLlMQADx^~K=gH- zY`b_#O}??m3$ihy$}pYa9$gcjF|0krA#@x@D`Z>HA0-AwWgkm9$dRA>-q2m**JNzY z5j;s#sk_DXfoLVAD91WX#o9W$Kv4D@I2g|+yQ_>%F^5<;A26&PyJ?`LbR2@!l7JZf zNi~Q%1Ud8jGM$LyKrGNBj@O!F?_`RQ9$u(xvHS0y>ri9K)S(TASp%q!bW3(XLsq(DSAIRYj9Cz}y$XVALDi%(nXyqik^UH+VK>CJxJ!F;G8@k)VzK4a@z+CG#}AX58jE#xHpf

raTbYUHoX02JhOY<3n@RWVmcHn*t;4o~j*WYZyD6Ng~hpkg=!vC!@i z{vLxVd-iusdSH%G4%Px%NLf(=WRx8w^IaiwIviqPcUYMh_1>vI&GQ z`$>t85^XR<-QC?GhDUUUAYpXFq9Bk!YXbuIhMT5ZcmuK1)5C&T+lF{H^uf=5=}wl33W+Eezmz> ztWAmU)@TR`lOilY6tu+8lpcJt4T!x#B|1dSi&VsIAGT!~!(&5F4ICmnHikf`(&i9A zm+vqs;mfOuEONkA>~3=MfP|;vxQZxd5Fl~@1ZjKr$u=N7RYe(Q4{$#$q>^@D4@ILl zjGIBInlf_?N|jEJusVdBl*lZ`@>p|B%*9gIaAb@9{8M5l~Ke1J>m~lM31m-`q1sMfH@LMH<6ffmFvV#PM5K4gANf- ztCz*cZGERk12xODZT1EcYVdqaEBKuFFj+Yqpx@@_b+Hn$X(`}_{A0q}ex1_%5jV^I@` zq0K%kM{GLF-fV8i)~M`Im!KigT+EkEb~jngrnk7EPJ!hdQir2Kgc`i0VU8lx%yZC7 zf$*05%+i9z*+T)^QEURiWu@`e<&dM4KcJ6ETGorLcXM{xgddB7z_tVp{f(BWro>K# zNi6~~IG)z*okThi@y`%6*c9IeEk^4G-Zi)~NJn@9h=5_F?mLFOUjOVRT|@(+U~L>I zU0NI^0f@4g zn!(!e>m1$64@1znhzAN32q#Ijs!5D1AqmGEg#$tD*xo3y;~D}- z-_CBYAB-B{PZfahheWOvNun&6>W4oT(mNBqP9UswDW`GOO-4IgkP{8bc->_U7ttvK z<;sGJ>2BIS@``gr?=Rtr3HtH__))5CQ=<%90EpfJJ|uh)He7;ikMM!jTKHowIE;8L zMO@I)E)9i{e_TG}$UPf7jT42KaizVT%`nK2X3 zoJ;jizf#B!gw&X-)UVF)AUSx>#UIqJzkI8V>EGzV3SR)lXS)$mfGRY=Y(5C<#7fEa zmj;2j9i?&Oih12%-eqTijBws`;Z}vfehq>~)?-J4$XG0tP#n&l|En@M%)ZgUeTqVg zB5_~`f;>*Yz6rJ~;qXoJPDZlA4Q>tURFHyB8_jcaVcrD@u}+6Jhe+a^bB;#5`j3g0~ICn{Nup-2$VjlMg*3x?J z?+UlEx6Du=>)|eLhrTy9bPxcBm6gX0F{Qo(A#SKyU!4Kh;<;i0S^L?2L(pM1UtqGX1%GR9ggGJ z<>1mCWy=PgD0HA?1N{~EUUjv)s=BJxvNT-HEDogcc7Of8@BQei_k`D;L|Mj<1tJ<) z9$)-$rwBFu^RukIOr^<8VsafI@&0-sGrU>n;)H)Q6aa_o>yO@i96}(+-R|9-!@jq| zYfqx^pmTtD7KtF?mp;f@#_`3&Rh4V=S+WC@IQz&@fIlC{(i0W9F}e1Q;lqc2!S|;L zlroTHNI^LdsQ6pvq~M&Vi7{mr!0883~&MBO(DgaL5q_g!anCa_$%o3oA&!|!Mi_q}nv zPzo5f|I_Vivd+MH?n-&KG$35){z=p#!K-DYdVlk9)mm4LSOA$|NKE^2)F51U8fyBA~Ju1_)9VU^vh_9E|I5|2{47*t;FFV-ny6i1h}ZQYP&P4&Z(AneA@(TxSwL z@=a__BAs|92@kgsNI<1N>Bkq?a-y}4he*xX*M-F_HUG!odag^6aNXYUpWi=Ln5|$` zg-``1F#rhAZaSmM_)H@YG7hM^MI)s*31CK8wou2+{hwd~u+86(=1B0;t~=yZmI>BLj&Lmg=m!2y1QT`-Kz zgKkS%j5vMEzvCjrltb&JQ#i;r&KfypjJUmfd6lT;1pD7^m8ntu;~Rhd-T(3Gx3Jd| zHTJ+Lys_u$LaAEAu2gtUVGEf7k;{v&`Y_AaKhT;?zzBKT4oN(YWFxg-y?VI0xVQim z-r=Ug>hovwNWq0xJ2|}5Z@&rF<##_19bO5k{52c3IZ2i;EX`Uyl6_UmCQu*9@kxyf>w*7fGs!--DDMQ z``b#TR{v$x>p8B!94@k&MHK)=Ee-`vRmFdrV|9?-gQ#BOtG@K?1ec)-AreofA;#Wc z{FXHPyI-cr!gGd+QKG5AxaZCmn1tjGd?*@%<=5)~Lc>Qv4FeJ*mzwpth>!z_fvz(+ z?yHWal8-D(Z$JNPcXMwo^Q5UhpqR9Zc`|aH5C}Ph_NB1mk2|ilVKo}frX2*J8sfA$ z986>FsM`{<(9<{XzDgN0Aaq-XNYVi{Mka+Am>W+c7-3b+anG4?!;y*;z8o?vurp}@ z0SaWJT~q4^;E?A>1%OQOhh9%_Uhl6N+;AqMewUG8Z=t4d-J)wJi|L|h5`zM;(rDdG?y@e?a)11~5JreR1EyCS;_S26xDvEyiBM(zekIBt|1;hS&I8&9UA!C*Mi z7OGHF?A(@Yy8#lXr#E-iBOE1WTIOuaF_CYaBtO`;{GyH)97m0sgv;om8qVDq*a%RNPIf~)IE5S3a^|P%3b)xcmg2Saz;_0HS3Av_)x$PjY!b1 zFfd$qq7er$2VI{*vI&`J_eiXi5V4ocGSd#1^Bshl#E?B}_|a2~#28u8!_N_qsXF(v zb31yMXRf_?u`3nMh}eQo-B-r6tj{l(pb}^j(?BeHosp*nfODqsLY^)rLOip+!qg@z(I+Q)b z(PYd;;1q_#VrJfRLN{gd)F9ku$^{Jqo5pXfS|tiuyOsb5^q;-XKrCCboD5i@s)fk+ zB~=NCyw7%_ND_zIuG}Q-nCi>y-QPr3kUFcimJk{uBRV9vxy7bp}lxwR@Ow-3vlF<91ywP@Nn144MKwSZ!-QYd`RrnicR zb{rP8$uD1`%jQC3u*Bi)3y7ZZ^vM%0e&s?zNUD@?vZ9;>1{Nu?9ubu2?B-#vw3D7) zxa2LgIFRTAAcQD($}D0NEA<}iMt~vttk3;sBV5g}Na=%6ENrD?J#wNCVuB9xe>MtC z!gfmK{Gn8sTM&epBWV#4AY@4#J`WW15;ihxg(9Gl#py{Q2-$qO1R$8kg0?`Sr3S7l ztE=TN28R#DO0l3bU5L4n&TJJ*#d0}ESGim)?Xc;@ihLnn927N|cr}gv@PFp2QdQ7o R0OaGL2taTt+ckL@$e9SdL&?Tt+mE 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 NR)VX`Rl+Po{tJzX-|zqc literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v24/ic_notoemoji.xml b/app/src/main/res/drawable-v24/ic_notoemoji.xml new file mode 100644 index 0000000..d016e35 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_notoemoji.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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

)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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%0DF{1?P_D_ZibZ^aZzCe! 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

xj+AiJj|ei${<{Hm8br z1zg}~GHTdXOMkVSm#L4}N5h4d|GMuMtGnWaKP9_A+w7_|@W6#aW_R7pj^DAlz7}8- zbRU>s&9g#tRYpeM9}$%?n_0fJ$Ah<@$3)ukKke6G8dS~@`V%X&bmp-^_NJc}8}+P( zqY4gG^;!F=E^Jc%omP$nIOHAT_=R1URKTXX_xWgw^V02a6c>Eo&VNNPvxM%Y=5KRP zB6gmOhft#n?W>#Bl69n_pv|o-2{cZ>AlCi;rSdFUxP5dOusQ>@ z7%8UMNC_0-t z=ViuFj_|s0+%M0Iz1iX-ruqp)d#ejPBg;K1#i*0++y|;C&j)8ILA|Zxb%csD|5bfA zso0{$@X5zHtHG#puNr$!Fco(_|1d&sXaZ_;7bVYc07FKTx z^8NCZaNla;q+W+j_JjM7I|!D1`AbdMK05$L0IF@mwOC?4rGnG8DCXGT9G2i`XqMZm zAk_gQ8qeX}MG{bCQ_X@2)uo0pda_WYUV4P__q_XhH)YprKL>+f8IzOKW63Q}&zjyXe>IcGfKGpx>*fC@NqRw??HcQ(VP-ISwYSqmT&Q6~XP6Ds&DnfqLmZAcx# zkdHEeE4IKSpUcT~*m^X_ATyxi=wceM9>6r=DzKId;TQB6ITL@HZ|^6=eSZ-7?*SR< z94{DrdVe^6e4r6*aJ0~e=uz&H?0y)rZ&kJ)O6H@L{9Li`jgVsGe-$|Z7i08?RdUy*0OdvT2J((PI#QxT0amAv2d*Vg zklO6K6;1nA!FB-RH-YHW`EVEeuB1#ap>vZ&Oi_M zHhgR|Tyj8hm;!dus)!p)7pqYO%H?0hJ^Uuh5lClHY49wOF*foQM)9am)Yzt3DTpi9 z0K5EDp@~0TH-g?^3Qy?Ci&Z^Gpb_gpi`xik!Hl)6xHdb6r*mA zgsF=~fSZsE_CxYBorX#`k!MZznSI`WwY{F&?lE_muOmk9d__hydxO){f&#Y; zGoTT;mD8ibAk!~;yQ*JHlJYU03r;?!vB%*E70#nRM~05esx-8;&Zm&HOF4n=BzIsX zS})QzIt_G?WDP6@>B?ngHKh!j)R`y=L3Gv7euJ0n#>_*-XvfEH$`YxZ&6-&6-+tj( zR6Ho}{M+m_Lo7VFtLNvti<|9wBaEOyd1##QhhBr(NJ7M*zF}?HH=?)w8s<(P)+RP} zT@|U(D|1_$se|&~h~eG>iMd*sd=x?e5XP?wrEw~g4^nHEbW@T!>$^lpDTAZz^*d+* zCcvU}K2CTMY=>G;Ni?GfWYdsl)Lq6^TWFzH$?U!___qh@n$yCws5~YsOaAcEwmsL5 zF{(Dx&_TF<%BZQ6+@O!lUiE5T{PNXTXCYae_?RK1SN$=#l!*yNG`Ha{pFFmlicY|k zv*pNH6^!Da;;(8Ar~rfJ5sEGeO(LIQWy5C7UdTH)?Q!J=Mtm{lrazZk5jD~{X4j3a zNTca!IKtR5_^_1z$p8MTNa^XP^=clWi!UcgrK>D9GlnCpI|hOOqtPtCns5LGDoA&O zgaXnVDIob%IwnZ10AaxBkdQ8E1gQ~9iF9n#=c^_Ci@K1{TaX7wPA@bLB>6*39A1IK<9q!IRT z#BJ&m)Y7iJ6)lh`ZD^_zhR} z*e1Kzbz(zU#3&g*MT?*noVuzDzO@jo4gcWn*{YWwb@E}VcyP0XalgdYZI-Q6ZLfzilX|I|)=c$gq@`f_~1P*tHLa?xkEklu?S|tUR=_a`m zQoD< zt|qgi@q85@S+_i1i4qzu=4x(WBJL!`1HQbNbbTC5J)IC4P>2kZyim`8nk4jnpM}oQ}kCb z5n{^RsB4RcKeICp?CEL~@46m}KZY|dx9A((b8^%GkCLVG7Z3Y^BoM2;!6z?iV^=YM zSt*<2>hv~Kkl96x9WN;oVou!cS>ht)BteJlF<%+Kc@dP`(@#iU zMxXf8mEGwn+EuctODelL)oKbmJQydu2oc-OB%7}@(?t-R^>izy zNA?H#+QQl^tgC&aFdSkiF!%En?tRtI-?M0!pPXJKw3r;108rP8KZr@)nlP67N=u8? zTP)m<_w9sCiNUz(B^OB=F~LD^br-ESE$E)-Opn%xECW8CIMkB+G>?^!C5OPnx!8VsJh z@=Mx-;k3WJs-oxm0k*hqK0pZZZ}p1s4^_Rg85Lg(n=E8Df%5A^G-g(X++kpCI-PSm zu5bTk#i*gcUr@8&qDZV|`6@W-Kt~sD!r4wN4%%|K-mxM(R};XUcV8E_-_P ziqPaNl+Zi@(opa?)kKyHW?QnbUZilt$^T^{Zc7`R2t)ywKe@P zD4<845fC5RXP{09nEn@vlw~-O)J1^LrqMV<)6-C~D}~uf$CPtzL?6fPI_@(X zZGMLo`QRy)@%@s5nJlM(vgbw12fOvZlS&`DkGf(+B^6>YMJQk_FZqa=7O|E_{VXsY zdq>L;LLvUNkbaBb`uKfh|<#&RJb7|;G_-qauL`S-crixwM~xVeXD z`HqF6Nj_m%_X{4@cJm_gT=(~{UpO*|hL;%=+G%o8 zf7o}NT~74kGNNVw_!-f5JKp%15PfPy&dU~K0({Tp9XQD$$Hx;(tg;+@;V~%li)Xmk zTHia(ry-x0oR|DFM9h6}O5>0@8At;Hn{=+azehOg`#>c_tXdNhRhm>i z{x@?16+3stYulwI%Yh%f*s4jTVtDpcj~EK`R1psK5#y=q{)!YUNrPaQIpGRht~T3i zlV=0nWo4{38yxX4>G!J^eB`Cvj)7*qf|=l^8&21Y&AYrtnr|~Q-seyt2_3}66P0Do z3*x}>bDML%VcY1{=&>gznG4t|EkZrd<=;t+2Wu&dps8EdvL(OYF5epJ)8o1=KQvtT ziH*Roi=%Dr;9ZOIupKo`IIgqu$!r}J@@kmpH8Z`c6 zPq!w_w93Y(A_UsG5Wzh=rQMY{h>zQUD~>aEo=>!XW8)dq!nH!7g!LNMaglY4ME#%fl76jFpgjJ_?Ff7;%t|4H&O)3-oq}Ogk??u{%yfQB$m9MqCbUWIJovWJyEe1=65|e-0 znE%}T!31tB+L$XwJDl9ly9o>^5@Ewx-T(B z>NqqR|1?!QiPJ)pC{_Fo98WOa*fjDLONJ&EQ5hFnbK)P7r&Ts)Pl) zBmyrYHiCy1zdN-)wNyab={#|#ZkG+vuNI_+o0#98c6`%$`+l9x*2BMWYk%o=MJC>M znfX_bhPQsx!kyiffo4YrvapCPdYW!qXjfAWV1w4C%9Ld!CQL1KNAKs6^p6;Ah-A|` zYVhnv3HvpV*I5oHf$NwCYB^lzyVu%_QE03abaR2mj2ZvWN}*4mC0E8@xp;_|Zm`OE zIq!n^AFXUs_>YG6YVUx%UoV3X1&HBcVKwI3rYk}1?EDoe#8%86dza$X{2K3AxWc1* z2Q?nqcUeB95|eknGW;Y>gP9Tcdai(cED~b^d?pKYysL`KW>h2SvBNI*XKHOYkf0cK zyIy<~5r!Z6ibMu1=a|)yU;v?*pdwiAcO+KqlXW6cR69+8B|hb%IzIhetajm3zae!| zt&{IO-66LP54y;2ZS^#NOCtdvKGOBxPLiFSJ&RqbqL;jznY9AxU1+m7Yh8SXbGA;A z1ilQ>y*DMo&kZ9Owkd9Yll0P@iCXkkaR?w2z&ll-4Gd(LObAG$593m3h(5TJ*Dw5n z5fo311ElSZiZM_{J>RH$#e*$S;{ny3H1haqfC;G$%d^R*%k8*pq`9J0i{Ni|a%SwO zo=E|T@Qn^XzrQ{qN+yJj#t-=w?rJvzY?eG01siCYN886u?IeNe7c0cOt4u0g?u!!v z%%8)5Km5iRPyw1B!4?mN?d!1gv#7pM3(ow`$Kr9wohFuUHVC6|8`45*X|MVb9LNwk zBj5ubQ-qzPq$IVGZE-#K4rn1iFQ>BL?g`6RuDHXclowZ59cjof8QDJ^keK?<9%U}~ z^&8o?Y?^bN2*L^u@5iRQ*XSLe6c11Rqr)&~H=rmE9*g9d-g%#qenuyWIjTFf7$RWnm$`F0&^QP zx`V|tx$Mz0TMQu>!$(ErMM#r3bJ7jebtUf0t=J?`c!XL%r3kCCVeiC}WNjl$m9Qdl zrr(|0gGGW3)9lYIG=#B)uReVG;N|Rs-|?J&QHJ7e|C_y^f$RVpKv4VrkH|zqBTC(G z+4rc%=8DS~y#i}vjdW=De$9#*j@P`p-$&(rLpXTbCVc+t35Hsq`skjYy&lB0#Bz{C zO8Xlc1SXdD?&dy?+n9K4(xt5R>qp%e0XQr6jgY)1gv7SxAeS!HZV}lO5g2^yf}xA} zb?0!X;t2(nB zB#~E^dVl7dfceOsIUA-lAo;W2qdGf`{{#y;T#VxSl`>AwV|~nBjcoFIp{xSEc0n+2 zFS`Jy{XN8I72^qA`R};gMaqsksqSA7ZWETxTk)S>li%Gvccju_g9nDF|G#j7UtYV_ zTcW;MEF@gBodm;ugu{m6=x-E+%7apkkAx{AMOqxyK6Hjn8dsr_go7RY#*!$)cSC_c zML>MRrdYdv`4GmGlgP2}ZY`NrC$|4Ar7{Qgl{)I`E-(B(#~@y@n2GD# z%TAKIcfB7QuDop)9A<2x?5! zndrBm`$X>xrIqXN(V=!rE0vMSGd168yT^5%b~LT%<-7+icBsS0=o3w`f7HDl^KgCC zT4|Ks7Dkj&ljoTV2akw?q{`mfY!q}H2cTvR^{@((sD(V@VHUC^pvxL{Kpkvy+aIsl z%c-KG9Q^}gZvK$}{>;5^(BTpGtDUe;TKl8qM%Gr`4%z?l$cF7t_Uy%X&_PEY1#&G_ zXjZhc^`=^Lhh<<2GStd)5!dMvUEjvWj%x+hM$j;~Sxe+1&!l^AO?h^AXVg>-SgTnQ zQFm4M<$qIFR=AyW$dk6>5$3!6832VL4=!^(Yb!ZjGE*TM&H_jpkIOCi)&^bmZ;xxk zD*X&yBVf60H;4{|gI}}CrjR1AIYFyJ`ZGG+iQ%z@WeDqvN$$go_naTNA%TI*2wC<$ zm5Sw!$;8~9p_2EMxgG`7({QHX2h*(Tuaiv}m-}AKCZZI1YX4HLQyF&_{04O~x1-IJ z2a>CF792_CL|ffVP7mj-q^mW%Fr@%w1RyWljhjK5c!oZ5EKuYRj+_9W#5~S%ai5nY zfY_)fKtNGdd?LcLE(~#fh6BH)2Br?4!RDFXr-Fxf>jf?Cy>2W*q_Edt-;9h+_fUU%@lk z&Q8hmj00fW6xCZvE;IS2$JGWSCE%cr^`_Ra1DuJgOa-d(?^i?&@vpQqY}DP%AR( zOIk@Z*~9t6!%5TCw_#;D+LBlKSAln5#!o0rVL`a)(6#>k4T5bz{AxaRz)gs2MglwE zTpVCcIs60b1c_3WQv^XQkS5x6gU%%bmn5EOS1-U58#QcgsTDgy8vo|m09C+nsfcAH z`7HwqeAcYT2?*W^%)0*arIgv_&7XI?jz`=_=(c@A+HV@2v z!>>AbxA{4lPguPbKRjW#nZR4B% ztQ!0?POMd4mv-whe>akg6-*ll2n|xA(mCGyg$q0_#T{7Q4p9y(Q5^kcQgr^bKLGyC zD!WXq{?)Y#($n5Eknoq0k|KuDv#aN}6(8C^s1&QF4e_X97d8ZLr__R&5n8Ti43~n+ zPbw{0GA})IDS#d7712n~?1;bdWq_sO<9!}Hzc=F!Tow!SaAS#k)z={WU^rxAi;sL^8*}D@BJ5%s~LY(4OUP>~sqgvK!e}AbZ+JRIqVSAQohX9(`Z8yEKz-zyF@6?Z#5=yCJfl}S zlddH}9`~WVYx)7paDeENsF~$AeTv^3Fs`&}K=)jRn)|t|W@y zeqo`Zyq0P^LlGPV8$y9Y8P6~ZD8g98Im{9V2 zaW?``Nt2CsPEqJ(b7KksT~~$NLG55O)1A7gmo9fKz4odxTLT~pxK3f%?B%NUe#C>2 z0^02{0lLhbx{1{aMoC4b5@4QYaS^L!8TP^BcP90YNC^P?(*7~|vjjsDrpK{*eO3|f z)*$uA_)u|l9Vc9S6l;hzt@z19_CYw2(Q}RGf`s`R}2MAf$PS(H>F#G1XOu{T!b#w67@Y#<$G%fk>rg z*hNdVH%UU=Pz1*0iOX|UTH17dO;E1a59p}OPew8lp^w;my&pLyOR~ed_eG%dgWo8X!F_kXeo^2*x0!&e%6(Lzfn|AX9A0 z$PEY{$+PoyUVOtY!4v`F1RWVc#H;Qyy2!}PG<9#C19~hR!TJ3~EoG)Uuyv|JH zM0N0B)P0FkvOZEH`b{zSY{Y-sZe0l9&^$z5_m{{gy6ZjX360pTsvu zuB4}c1)Rk}_)H8yk(zY7uL8^`kx?Gc-cNA=x#7aQS^;b9lu-^-Jr0MW$>^rjzTMbt z<<5-EjFt$>sO6cb|27rnrjz21#D0y;*0GE;0!g&ocf0a(`0f#>Bu}N3995LD2h7?q z@{iWImg-~>Q^;^ycD$N0UJ}ML9HWeUV#E{cyHR8(Gvgu(t(qYEBA4SW+Wxc2I6XUq z?PdYy=fV z18z>4rWK!)I%>Mag>U>hTjq7FT7Iy+G<;s0XK68brcL%Luw~TJp&X<-$VKk=kJly5 z4ZJj@N5s_D?6+`$QY5dhNZb*{1Fn)!kHxhaa2?WJrZlj=d36%hF0_O=nVD)_;O_e- zkc!qTiq#c&DGi5Nv|8TE`1p1o(sXhoNps{RtclAol*Cv3n_OP|tD5TO@NywUDi7SO z@{Ff+6TGJVk&!^OmBh>kf!KO0(>iJR4S@#*RkpO`kW2e`)_Z&Lj{2P>RG2gSK|_8i z206UpEm3CM-b(#3+UOva?t=|ac-D~Ag>S-vGgA0`rAY!C(|aQbjgSJNk%EJCT?WqW zFH@D6{J5mqrSG)Wn%aog9jw8YVfM9M;``B$=6cCLDD@7pd^@&OVL8*=nZ^T{r!^?K z*CvF{BJx~+@0w*~ZZK=k{!;~N^Q((tP;;$d_ZN;qY^vRg8YpSbe7i!(2E<+Ffu}o95R|wcO6b>Gt zh}BL5wICUZhbt+8%1nQ{$wRb{_l@S~n8LhF-nN^mbX@9WxkW>jzgSQ>{eEM-Fzx$@ z+{N!!sCfd?Xj^#mrBLdf_2Mz-Kl>!rwnvb#a5_Vk191k-{N)^j?j#|;M3q1nhdg%pX1WssX zMCcoq;mBJG=ODxlcR-4Z-ucWNQ5e}ki-~gJXFiuROy1r9yq=b}mpv`{Z}gph^pWzF zLqMswLj-PiNsJ@pbB>44O%a{YqWLIIN@eS}U$p{I7j`eY7}dN}m{5`?4=w+?AS+?s z>!L@3Ug@+~?;16MZkUkkLM^jGt+Y07R2@}S=&p!)Z``k#qy3p<#O~x(2}Tb6iT?7D z;YS9szH#H}5~xQuYT-YHd3liG?b6@-7u2wN_sAFClx2>ub8;1`ItlahOu1l>DF1$Z zL9;F+LLliD&W}r$*$yDoB?a*w=*^M9iX5J(VH3yeMS${jaIYUlqD)W_4@W zC{x?c9IxGmrTvVh@*j5e=md$`1(i~V`6Zi26%gNw{vyBK38HDoo|o#+5aC>~KBfRh zd*Nu9aIb8>2~}DWwvWVJ9`{8v45G{!4aOcL6baE?DJF!SSR-AmQRkWC>b$H@>+jdr zUY=zg4Q$lq%h$U{q&%SiT-c(Qu+F=gEnd#HkGTkoT=^g^^%tD(bwVPwP6FmT&QObu`I;hp{nAYT)#Xnx%#!H9=8!Wq=Bso|#9BbK}=ev~k8w#|NA zZIP3b5Biuu`S0#kPQaNg%F|JUQYD6_E>M2EWS~+^mo!=Z7W6*0B4$`A8d?L`3bSVb z(4u`VmmX*U&j;eChPjft2p^!P0(5Vc>MOs@J>(*kY^Xd=#OQ zqod9do4kI$ABis5Ji$lZEaVLsd~u>UIXUUJX;0S*NMSl=dRH`` znab5K3fP_ccVeA`XSl|nneB%zu~JZ+Ih*g4TS@-bQN!wg;fT5~BeRaV{!&ujo?j7a zePmI)BBp2oaAYRUqMG}^)5hvk`4{+Gt}^lISNs-IU-MGbx-II}-(+wZr0@R$JEFDZ z;nqhiKVNO>Rc(CdhDQwU=b4rG4|1E1YDUUYDX7M@V^F<EGNGw8K?Q!groxL)U_e@T2keKR6Ay%g!_Xvl>(2R4@sGNe$>t|_&uVVQ z)2J_=T340vzzs|Wr8Luwu{WGdtO)FCk1cd8sM1d3E>Hs0)xqo>);T{@8;`25#y7X$ z9Gv5~}npL{71qRoyU{vW~bRun#+JzS>-Rn_)(Cm3Y_ z8Bv9O?dU-GX>r#leM>BBnFBT`epHAr`E^=;&?PI^Fp8avuL?Tc#XnwR_5Qn~XiZ57J=Zau}`|+098ll?qYwDsJemI%E2CP zN2f*}shJgWW75yTM>F7+mDP&!Ld!m?>O}O~FMhOsRNy=S9xNgX57rZe8%$$4nIw|I zquO(m=2Z|6eaY=XZq^|ML(2B4$6wW`(rSX7P8CkoErR|7?T4=a2IaIc!+a>x9CT7nbnG zRhE9hdm?1)gWSrs%h?lvX=o<@hoo#pR`a)<1_LI{^?x<|!i6RTWgxgrer~w`8oM1Z z#gS*06JD5I5Da_{FBkf$VaD2Ofj*l_&wqC3L@}|LJ6@5did-?XC~SW3@&7LXvTJ}! zAR}T@biP;%QLsk zv8<6SLe|HQLI4{nR!c0B2eiQjgI8_iHuqQ?WBRUMr}|;z+2zz*%h_^L%U=sxIJGhbFf=<*Lmu1lau`_Wys4(34S(xv=;0f zjT3S*r|H?hO%oE7zwlWh4Z_ zOL;+piZE?HHz73Vd-|#Ia((7__RLl^)VxbsS#rqcg~>tf)6KQFHzw^blMB_cx?mOT zP?D!F%^S$h*5`Ovcc};2F67F4(tIRkM#WNgKKJ7_M*0wIzb&Q}6#z4PUaQ*xj^g7R zerj+jY=`_;8@Jo$FHpp1Ld|v?mqG&gHSGul4VKsI``4yk99*e<)V72wR88YH4urZa zIGuZ15b{ptzp1bp)7PHUxI5MxLB6D(ahmke8te|k%dwR_N z=gl&ShYD!5Bxp|X9g0^eO~H8O3vrY)QY+$^ZtwQljNWw6H#*d|mq3Ii8>cZ}OIvU$ z2)TaJbWIz7sgnZqpoKx2(7We)E>FtAv=FuS7_hby+Q4AOcfz{~;(hR0;Sb~AD|H3q z+hoQ!5HJr&oJHun=iX4ssCvESzmiZf887$GxLHlqVZe_rLeuJnpIIF79$KazoJ8Ni zKX+qzK}?u;(1OG>o|tMni)2xPQj;hivC-MuUf9kb7L`xNPy8{eSl#`yj+fHfxrODY z@8pJBSEf zi7$AL^Nhr8S|n-N2_e1-qOo;R1ZJ1S6gl}dHB~Y%Z(_i2?J07o zXGUD`7q-Cn&Lcq;n_cJ-aVq+hcA(?ln9ztL3kEiJpCC*#FoT64af!UKkb{4lw*taR z-+bP-I6tG0=z?~wlAnA;6H_f00``4Q&itbAfcj!tD_tWKy2X`Idf1UHq*f^5C$oTb zY!A378a(iK7f@h`HPU|-jIadT!`|6mz-K^$;iCr|Tsmzu=@xo`WfCcEQ? zO@2S7|I9g)u6U$~-1l3rb*5@!HKhQpN?#0)*Bv=TShPdyhCEX9(z0L|mWI4Z1gPQO z4h}IeW8IyUb`mdfT3M@}8CJLS8ExJ-pt$LPsE+ILePG-kXZCpf<-SKE`r9}1_1;R^ z%lFCrP8IrRuvX5o#u5?>s2X@+$Gs*FnFS0wJ)mZ?uNpBgxlj z3~zA;*uc6fl)*64eLt0y(Zi>n|_HYlv4EZ==L%Jd@g{+O65iU)Qv9q(8D zC*!hTd4t2YgTDASnK_aje(QISK_df>^fK(h3T!m`sdPVt(w=i05fQ!)DfD$844pR^ z!u*lC;Q{f(h`HcPI0s%nkYiYnzTCo)&>;BjxO{AIR0@*~FRmAS2XTxbFD$j>e20MQ7JPu=ZbZ6((_u?0WJ8k-T)GhIIgg#U{TYE>##C$H9NgJ$8@9rv_$)#*STLa)c11pp}>k}mET+G5;Q`++TUZi zz#sZaOtYM5A1bab)Cgk=)v-avaBK=rC<=0wYO%jMJGc4-y898mGfzY0pEI%hazn>L z@w-s%I{!yZlPvwsRl%dQ1v(8Vy)QL9UL`9PF=PLm{<}nNS!b1y+?$U6m<8piym?u{icy))rVsroU4=pGleilZxrwTS4U5gCrQoNwHe>xA}sq>U;DeT;hd?jAnLw@gS^ z0OO}2przfw!Q{{43>aQ*gVgUAD$&x$Vt}s^B7QUn6_jrGxinuRC`sQ$pue-QXGO_% z$Nb5QMn!9r@;H%pk*_kwe<9fI!fnnaI#@BkvHNB$&|m34-sEj_JhFV=Rxr*%Y!_fVzDD_&>;f=e{@ca}Uh>zas;ATn`$sY#=X`FFSV zZo2SUq;C2Z^1CV{0$#Q6<-NG?1xjZI2NpPF1xQLmG+ikC4S?RaPh&7OT{w5^14*|N z*ws^niO+UP7bDlfzhA%Z)tlWi7kDkdI=K3HoLpXd|LOA;=*ft3f0F8Lw8=29Nt+Ch z+o5D7zwVugm1=wu-}`#urYouD+gllo2P^Ae0i)N9QDBdTCdcn;keYMue*8StICRGY z-a(s``kVg%hQN{0QiSF5A`i~hoNwq${Ha|_!ss7@`)~BHsRyRMD{!;bimuW)vc(J5 zEb{svI}%2gj6bZ~OS)2lsZNv^NT$pqTcBli`@`k!v$)?4UeRy3AS8^nMH_BKPae0l zbfyMMk%Q;JD5wy2?`oq8ah;BNgyL06BR9rx95c`U$FopRb5J>lFdK0`66bRC^z`-h zY`6aY7#UYP$ojZO7lTQR#$DLu zj3@wnU_oi7C^u^vZ;-$Y7@$E+`F#f^=$;70(4efVJoi%=%U`LhGL6rf+k`rk+v6Wl zCZ);ik>vju$-Q-ynMO(}G%d`d4W8u`B;H&bxIhf@fNT#&yKJW0s+G&Nc}Kw#lEW0A z@N2a}zyu+vwh$2j2K?|9V|9!OJQRj2W49+PU9)8a;hFvEFHVRWDH-`+FboUhQ&J?t z3$3`y@!}&}39q(OU!+^Js~1yLeZ(;5J+Y0lT;zbI>R~@rWkkA=8llr)YAg|p@^U<- zqK1&*un8k5-eF?lf&)umza?)I&C30la{8z`wL9Oqc)mQv&^}s!Qb-OlRJYzi?HA@3 z4KA-uF3aX(CY`-L5^tMK$k?|@f+v< zl*yNGJKla{|9w~!OkBLZi`X2PwxrEp>+WvjUwA%9-90sF{3{u@)To9%%d+E;hLa7Y zk^le}()CUmv!<4aa>W=rpc%Sbdu}4}t*pH(_HKgvIPeCL^WQCX6_>{1$N#cUxmmpg z!khk($v6F!s(b4KfA!QIe*Z%(DDv(<@`!*{A>$}E!ctS-K*$gQ21gG4^3Sm!-d;<~ z1(Sn+a#Ge)7ZQda*Tmr`?~KmvJ#oGnTO%S;P){-ltE;Vc=KwOng(04IEaR^I*IM;l z6J>SP*2Fi^y^!3HxeZhyCls#=JIp-56wT~rrW1i;l-Uvk?yUMJ0w0{x$YGY))`{i< z9sganP6YVB7+&Z;0p^PwEV}m#5=6ZFQVL%2m)oaKiYktgg9E5rOO*_KSAjYkMmm1^ zuNinkbE}|TMK@zfiyKnsgcmPUw6ejmcC=SP<1f~JKeeeklfSqTm9DyRkaB)H_P#a) z2rKJpubhHKBN?f(fe~)06ipxMS>U5Kx!hK3g$#g=N|*)DFec>7^WnJi%1Q_8%GK^G z@vuTCkvYn~sgfDs1c5E>*!!9G-{h7Xf2a8mJH@P&4icrKe1I&v6{^_TLkVo?(iJCy z_d{$UAPst{7#(~?IINTgFy^l3`zh(%x25dwq*LyKnK_`&UG!ytZ7@0#y5!LC!x+tX zRNa`Y8XbXHd)WkUFjZA>#rmZJ$c^8{Fs-mbf##`R%Ji-8*o&Jw+unXWZ&Pc6f%o8@*M?Ff-oP}F(} zn(_XPz2oA9wh5-CAOjEU#TgDMvXO)u2h=aDDJ%Y?<;VVt7o!TP`sZ4m*{*cU9Bea0ivggk=)j5c^k_w7MPpqGeQMM$vQcAyITO zHi&myh`S19>Hv8Bs^!B61h3tyqPU%r9D5?t%|qEw>0lFlgeK=F4v%>8j747Aw81WN zCaDoi^*AXFSoitGKRF&IOGUD|f(Zz)(B#*WJytROjk=)m_K$KR9qUuX z^+30h3KpvfN;{d1PXxZI4H~EN9BL4ypdk5im|)4d?9$*buNSm&+R z)t(saFTZF;%znaYzh7MTkDgW{h6z7HKl^Eor}U28xJBo{vM3gksm=*)YCYIMz*f)6 zqTf(hZG0-})OJ07XNC=M_@~=M+>>ZGI+k)0Em|Vu&XBn`*~G?2{sKOC*xjxC^_=kw zPsD#>t;E*J&j)oG4Ah>Dj9YctzjseHT<+AaW`xwN|LBGNEn%)YM5PiY^DeMFB^QKW zX1sC}V@Tc^m`epgpY>e0>sAF32D{B}dxoKJcK6QrbwY4teW?zLQAAa8w>np9!=wVx z=(I72z)mQBdMlP;oKf|zV1zLPV?aukA9hF{78;S8=$;y9jMeoe?9sE!ZVK=;^C=1| zC1L<30t3D|6tJzy>Oc}g)&5XEn%le%_R8dC#{AAHh07VM=eD-C#E1$D65{L{0u*KouhX48(go?8>*?ll{4DPU#|PM~Cj&-p8tE)A z0arY=W{^4WcvF1E$8Ub}vWZnk7fpfrd&ktGFDL4YiFQvlDT8c1DHb^uTryf9V20p| zEn&Ir9I$hWj2$ddA@6YhizRdH@}Rrz!9kPkh6R@#hL-`;ck}0lyESAe-B*JTUD@EA zTUNhVg}F^mPPX!H8M)toJJ*n9xo0*!GDaK<%C)E(a!YmZ5}F1@qEo`z_cAq}sh7b> zkM;NsaM%Wj%%Ho*0cN<~^|%?4Li@AIQne}(_&Gclffd`B*pGFetqpqfQgT(dI|DZP zt~rGH>X})!c1pzOtr_D}$}rF@42+Y(q+FD6{d8ZG$~ftMGe^yjl#XbxPApvO^DJ0g4b45+ z!NK^)~U`^(-5(Cxpoj+RlTm^(|DNON#BG_ zvyy}E?mJqYcG#2jb74DYrz{&~>u(v~G8Nl(TPuzEz$pz&$v167N@Hi&m`LVwoWPM}p*u>s}j1%w#LAu|FDeGfe z0(8$Bbj6j0{)>ZlE*Wd!7Gmvqo*Wx$bE~4Q#e6qG4RDi!#99l)>!ul2e?+raL#rFJDgGF~U^n`0O_sJZnwE@F zrb*^4g@x6A;L9Z0#m4)ehkoJ#0F9B^nb&)7`H3;v5iF!Hi(o9sxAMK{D%ipt_veUx zP1>K8o_r;JtcA!p9ZX8&s?|%(IgwAZyd;TRXoS03v0YIfsPs1!dYFxu!be^+nR1 zcr`Fsg&G;t2=_Y<-ZyU2j+W*c8Z7uabrO>j&aIDus;Zy5W(}mhznzihDInO5vpzS4 zrjqHY%K@K|kKSzepfFX)cTKfwo!DB;=JlVO$K5j?>kdQF;Vk6xv;)fcOx3Vqe=8^b z3bPXr6w(*g8}`E;U;2C{$*`5R6*O z`sY1Q0+NoNsU3{$l#|z+*$HS>+~{d<=(U#T7s^UE^V*s+HvzfC*^7&-fL zg@<4FDfO0cEP5I*0&oNQS3ZaCpck)Xakb~n`Y7mk%S;qjy z*JS_o)LPR&qFc<+VzB4+_fYjnkq992S%xkG8hI2ee8A#0VpOnaz|7|M*vmw#v#vIG z&hC@vfK;=|XmYKnXD%>s#Up5UTyb~V<3!Q+XMMW7y`>dU;{Nx2U~tEu+k4$kMme~m zsT!&9E0|bQ2J2^VH3__+uS*p>GDwz+d4%Pj@Z0eAp5gL$s)oEx5*uV7=jQUuXLB`c z^x>+9!OCZuPGd~AR}vCm|D9SP4(6Z<&7%1m03-cyP342dX9!38mPw#gM6r}VXd7Nc zbV(ICw?7%~mOcLtHf9lq8`a+2EYIiLx@Z&VSnIeoI zykR?k%~?{_^R&#de5}*wV{FuZP?^SE6UG*VeY$HrL)O$kesZnX z*m|A_c_#>?1wE&R_bY!%)smXM;>7JYBw-$n#=IULhunY}fvruyY_xo6Oq1`v4FLtM zh3ASa4S@mB>6NwC8(c5}#^-ZpYMqu8xVy&@Mh8dyh@yvIuGo}LnbwwJc@x#>b={5a z9zj^(ADJFwm{kEzabddH4DMkj;nec^07UM~Es1FWD;3+SKL`Y`W@LAuZXJo-=UL!@d=ycY zLg-Hf`jw1GZZ6*{Tb``3+UWwVV%4(frihp@T+yK~@_amAzx5$*%<5Teb-g}a zvmkVneHZeP@8ncvM4h%l`Hlrkm{w+P_Zp@3S6tp_w>k6HWnazIur~=1OY?wr)BA^G zB8G!oY@b+E1`<1Px-&Y&Px684B6rpM6m_=0``jgXFcDoJ_0a*D%XDY%s(igXO>|gw z-Jo0*)$1WgZ%6mho!xa6!giy>w^ z-Z$5TS^ifJ9_B9$xZr0WP-*%m2;}09ipk-ZJ?A*)0k@UZ-=`yfj4*0MiECmkD7R^9 zHHT?yaVDkAnMl38H=YB6sPeOBByA4XPzplNt^#mFi=1%x3yy6(95*^De?7FlR0YA` zr`|rs1$UZ?vf_q(nsi*p#OCpAod?2s>7Ww#2Ln-l4Ga01&IyB#gS&M`4Ggx=z^IxGyC&!eQ;Qp_y2bRE{Z(@W5YhPtJUoKLr zR3{pg+B6^u?{T2LCRh(y48B&q?_)Dk#dR9x$@`%1?(h$9CM|f= zo;?+FbvZKQk?eL0q)o+9fm}XdaA;LF#=}}Gj)i4G&OeRm5YaOB+MdOvP|Z4l%?pTv ztrYG>tLq;KFzP+ml-140X6;6Gl*g@CIZKu<)lVzlGeh{ou1? z<*!Lhj>^N=@NiVCEfKyQy}w)VBNY*jV)PWmjCI#>(&8kaT5U)<${iXs!2>66{NBT$ z;_Ux)sXwhMIIGsxxA%kcdo?D}b69xZYc z!gr;)#R-m^3XahL1!ja+~fvgfi)>k z4s8BU$i1;QE0Xu_Z)v1oG_^FaR_(iibmnMJ48UzvT@2hu_HNT+1|^ymZ`>*Z3o)ea zkn*N&b2&&uWs-EnNyv@6P(&g5V> zusB@kG6ru_IJp{n#4U75l=_g{ENeU&3+pM*VBP5g++cpc-u2R4QBpJf7s3DHqrW$P^Xv$55yf`dQLXcxm5OD5=qH9lxrMruFpJN||YzNy6f z409VHCrInouW3pw^s*81W0j@!eJEqW`Op1Pu1q`*evr$J4*NVocGjVQg%Fgb{cgKM z%MAub7l)8Oayb&zo?C!w^hQiW^3CSJwUr*K!>)X;dkP>x5eKuH8kbq z)1>L^=m6!^EB%9r&r1jb3vml{X%#vKGR_BfdOQ69UFI(P(N#q_(RRlB8AJ>rA%}fe zP%K3>9W#DfCPz!!?O5Rj;C}i2dC6hWeVdMQ_CtKxl`6Y$d5N)|b8~ABHum(R|Fb+1 z*z;bya=#Wa)KUaCXXrcp<oP4pd(fC;K>EDHOW{Ff4FopL6$`XDs9qxF*(iSW%O+gZ`zXOif@(%tI zPqUai+4N>hsu-t3Au4E*(&zQfnw{4Ra9_f{8yhpZ7-rf-Xeejzs;n6Y+m3PFF zbskgh;e)$2tz2rqT=431+2G*QV;711UfZx6__6%-cOLrJBPsc`enWB3T%L*}Zj@;> zpvkHCw$sp2eOsfNmSizx?H@50w49Ofv}51_Mr8p=sqIXzmq!5Od9cP1sD{~Ka3)G+(e3M=v;SJYd`eLa3@17QSrgqhJzoJd-962r_ZlbKFh8pc}-W$ZzVC$er{3M4W*@i8RuT||HO zqD_M)jBEkeD^{{`66A^cV{;fRKk9tSZ5m}}K>-lDTHx@n3VOLg0@B4~3rYUi z5|ob?S>Nh^rKHs?3Ky5|MY2ldYT7n4*zNayk9`;rc{`0wUsrlK1Yw3sx}l9iysjc1 zkN%ZgYzm%>P$GMuLZGxz`)j0f$IDh*r>qd&9=D#YKZEK(HP~{CQa~;hzVqz#z6M&Q z^!0+K(bzIEZ#A^35@GzOLoIT^`N)4F8$Osb#;^H)u@0_Ls+}{bkZ}jv^RS*P5|MdT zT67NdY+zpj&iui;4X)I-@OQyLGz?+@Nr`gIc!U&fyb05~tPljGm2m$K#O=QoGUl!t ze!VEFs1iEzoMry+^dOV&u$#}31%F@nBhkChFk2P9uB<)8P=io7!IF#R&faJHqH2C= zpb+d>Sf?TH;yFWJXflnUiqJpKps;lx8cP070k_1idH9VUyp4w};RGtuu~ox+RPvG5hr{TU%-`aMysz zV{yoL10$xj8$v%1Aj@0*76X}26~8=3E>#8|_lRdEyoSf$5$jIY=6e$G!h`WyS@X*^ ze)l8{DzPH5G=>+l=)R&+B9a4A8Q~xxA4KVnf!nNdXmm0Bh4Xt39f4+Zs=7x*+nN}; z6aKfqnU1#l`<}j+g}1~USzBk}w9oP)BaLBcg=_PfSamDi{B2@qVoB#K|gqo>nQpo6V zbs9!iTx6_IwRxboUaB8j%?464F|r!Oo8o%6o6;%m_ETHL$YJ!fjon7=+s?&vr9+P& z9R(*I6m2)jQ6s9urStR7z4MO6faH<5e?==kNi8@N{s2~sk&fR_O-YO2P3Hn62I>1_ zMs8Iimp;lt8D+<*XQ&(c3QpsMvfM37jeKSxx8cU+YiShK*s!lxDcR1e!Jk7~bM2<$ zb!dQ!qq23#kx{2P-Uf#}KrYU~Sq!Ll)Z*%EH}V0YwIM=KWnX#6(RG$-$M`u!i23WV zOwzUW=+_9ZJ+N-s2>+G};;yH*2Bu=Ic<=F|26>*8t-ItJ?p>p(1U|(u!tLc*gR-iq zfoXg#?>sCS=jKPdySrtmu~EFTl>S(odfbXj(YK$3MQft%z*wNqAc)RM>65i=a&TkG z+P9b|k6$$TvunTh(sfocG4r%}qq4&~Vn_!ZpPBHAE;h_^ zG(b?D(B*nL#Rx5u1{w){_^YXmKr#P8+W+>D>?KpP0$!5Ts^dNkx`%WIz}CJPTO0ka`as+5L zp$F5BPbcpOqcvZa9Fu7#OgqOm&Ifw)-xR8d-R zdpm@Rc@fyzQloMNY>$wKtHSW=o_mSd<;J+!$#;Uy_JcAmj&Ze&nIr0TAOu?@AL>cQOMW%+eM2?Wo5V6Cg6f2cofme!dd=OYaa+7r$&ZY>O@+5Oh3O-H#N zv}bpuUz2bH3StpYqqW!0sWQaOZ-4Jlwata75HH5hR+NnP&G*6xuH&wTd#}Mtoybd@ z5L2}$RLv^_sNN9Jubs#h(AlB?zCC{i1v$ZmT9iyL`>4K{>6gTHs88ExD0A zVZi*$>NZ^KDLcHpFY85j`-HLK9g2^lyh%)?xRaT}Q104StP$QW$vJDNtGg+%DOtit zxNYOc-#d>jcr&rtv;(>kg@9)ephjc{rX2My2V~%L;Y3_KW}7{@^vE`bmR7-u1efNU zx}=Fh<5V+>96hD_E!(Ut>xInl7EocY7d07uY)Mb3X)@U{)Y_BlluQTb>O(%@N0`3i zv=MY#+-QjAxOzJ^QZN@Cc5%sx<>eyOG;#sO^T+;;2}UqW`1H)j0Y2G5n)R3IteK-< zpzO43XXiaa8U&~y7t8-#?%+*9wSOhFK0Sh7IN%eW|QFkp%szZW_$b z(VwfAnVOdCq5de!;)dIkLFOG6kB|iLrSz_I|4T z8UC`=@O{{E9nD$eSYe73qPw9?uK`U;O#)W3lK;Ms>C(w*GW3NCze_!K&v#X5W7-k? zrX7U{5tH66n&CUPUSs?Az_IT)Ck-!-ja5JlZ8vWq89OU+p&UAjGoWI=4AbN_G#vXi zEQt1{@PUYQ#l`#7rIT*NJH8pZvm(O=Gv0y{h}FeoYMUB#E#g!woC;Sj-$d%3>zCBD zLHz_mO2GaWTTkvTZp}80r9+viu#am!ArL)!=4ALYJzipzI08QjU@BDM&W7+fu-Bcg zR#eJw$8-L5xTx=%)OPxS0rTSW)qO1YBRIox=H|u(Vs!QcN9`;-4TATQ_HFBXv6<-F zc)GP37riTKj_cw$i)5Ukq^>kVt^!nIR&yW2Z{K6V2`}$Qy+z4R1T!FGYzXg&5$@&A zN_>t?$0T&;NrsgLZD^QsWJ@>F&%TL(Q0sB#*>wxC^Ohsj@%6dQ-=|z^u;CJtJoT>* zdo^5SKs|{UW3Ileb*KICRnSXaA~2t6H**#TO{d>h%4mIZA}n&Udv8dg*kzWSr)la6 zcJ3e03FhqZDsSQJJB&@k3y_H^RK$Lz&R;xt z`L9*ydGRM|(VRSI`|xc5;u>^1D7H%BP;gOq&r}^Xv@@sgNUG0z3E_7yDC<6vIU2nY z(pQ6QU_{m?3DZ-(JDtCLR<^5ZumMl!6S?GxjvEPEObaeY;2?e|E9=oLX5?( zG0|<0Un|ZZ`zChoMN(7IlvUXM35Pp0fqZ05WpO$88huY=4_Fp;7=I?0sw^*?$zXg%IFugiuP{{GS)}K1_$Jk+(#DFgfdf zvFC*n;UeB{`s(xk5%q6cu|4<56QaXBwGXpYKg5V{X#Ay0 zyAGVP`1FYhfCZ%4`4Sp`f0}Rm=jq$Cg%nXfV7IC5nkNHd`@CHwU!d&Xo8Drw#_?yc zom-hz0M4whJDl`h7u~)#?>ac86R~xZy8t3>zn-*5`-b&Makg#dc7a0yz>os@>9W?< zhHI#2ltko7&q%H?nL<0EM3BiLGUe)cTNt-EoDU@MUg&Gj8`lVZ%7dxyllw$*EL_(U zgZHdjvXt8C|HU7P68asyILc%S{24q%4PSi9=?dUA4V*q0Fz&VU)%xromNG1}i9(z^ zNT@T}dr$Uixi?L2U2&@-cwKce)WqBpK(!u1EQ$dC?f66I2L{RA*9-t%QOR`sY0($I zCeb16V0H|p?agfzQT3vAh@k~-|FSMOo%gp zfJ@^$kPGap70ABw%+R}E9OO^l4h*8KQs3a5&51OQJI^)O#hLGUamPoeW_zjm>^#2) zc57g>={WQ&a;>H4pcAiqSRbVsLIcFXvLDn;&wZc_jgie0YQdLA{}$+yKX^Q1`B35^U3MHO z^eCcAw`|fpN;E=eW8r}1Tk55u!8P2|r~0NCZg9O5X-u_J`KJ>-Pfy|q@mtqQ19C1< z0)}yn>+8-V%rP+n;U@tQe3U-amTb&i$NFU{9ixSKDT}JlG3wJ5{qtN7zn&n$EMMA_ zsxQ2`AwyXtkagZ_aoUOzQk*h0PlXijonLw?Bz)5LZ7>8l`lYN5DU)262umWkK39aU z2;zVN6;rMcwdWv3w>Lyd+B4K1Wq@U1W6!Vr756>9r1;d!nASU;)9(GMH9OT$j2B;* zq=0)Tx6i`@@6$ynCR}mS1Dfll2^O^1lxYP`k0}_7(zj+io!HhypuA#1Q@F-3zm3NM zFWI~rsR#K7fF^$|4cm-P_GP%);gJTsQb3t8YD3ubgN-tDMO6`3 zD9;{!ND-)0f@CeWBWmD{f+j_oqx0GRg*`?T^ZTq|lRBZrkCEG7&7_h(eY{3Ac2`5# ziNoBn`d_dl)l zE7QEZ6o=pu96V@G{bs*It@lzJjkqwC`1Vy&pkT4OhJvbV=^NstjDLrmwFFO$c zv|26YBl6P59S`zAgmc4~_!q%Uwlh)3%X_PDNBn|eN#Y?_t_Czv1-rvYZ$L18F<8^` z@RzspqR#pnxfh+cCOGB#nFPZ ztPR1U2fMG~I6)J-v7&$5-RaaY(>kXM_vfsG$3WddBWZyg8BbiZsqd_V*5jbfqs=dl zAV^^u1h~0xQWZL8orilCm;*9vm?*Djn?*evKhp|ZKb*4-?>n#_mo_d?tdMJ~CO9Fo zN>h=!R%$bmafuS%sJ(89oQ7vKzsj!*0JT$Jux6w@ke1*V%}IfD|6HbK>Q-ekUwLy) z!A*ApT@nb8{^fng`d{2a$ZpKXz39D#b0?kXt9;@)pU|0|i+h?U-$F`rem>buH4RsD zrrmxN^l%0b-_f+Gmx8ASORD#uqVjp1SeCag%Vqzl&2Xru;cgUT8aTiq@}8~8x8Ev* z?G|Wp%Awu-=qamoc(a?1JmSWdpYn1O?jG5A;=L8OGMJ&(#k{xBg1ITPtrRdViIOGk zkKWxpTG~*PfGbTPL70UK9w(N{q2;ZL4r@$2^KXxW7fxv}uMl5%-*X(>;8dwE+^}x- zZe&+!3x7Qpm0&(?$G@BL`PyF%mF4yimii(l+>iku9R39ekBCFH&KTcKNXh%#Dga-X ztK7U+U`u=nnlLLft5>aTHD0A4%_;C{E#bATN!-dm%hQ-Sp#~|0fu9c0R?jENu|RIn zySxvYH=$bnUX1RZ_L{RBAvhLg z`wGjEe0lEnqEZ4Kca3PDbQZCg{Z6x=40|&bZ>tSNhjJ`e=>EOLLA+;_(uU&3$KX`Bb>NOk@tz9vA&yi4B;L~jMnNiD95(gH# z_MC%2%{{CS+xza7~i zDSTz-gJs_8V@XFqV;>$vcnw!K+R$RU*G|xr517HVdOz9A&v$NYB@CoCCE3aVUJ?bm zCp996kf4|lsI^{0@7UJBuWH}$x1UEyCP{}LwM(OlbKGcGKWuzn&$GR$p5rafh=+mL z9JPnHU!vy2r+DGs0T}j194qd@eERP91SZb3`w=u?fDXocM3j!Kn91VqAeb=DFVvL+ zf@8~LT|N;!iVJ{1gE@JjfIh-M!}6lw*YjD@mPi6iYo8yQv2NddDWD6g1^xz zcF!O*+XS!s5&Dw78+UV3nD}+F?>(O$~K9NEVS_4O)|0{g*=3+2o%cc0@w7E6&btI*nDPLR_DYEuWarGcLWn znQNTrNfG&2QYDU<`ub^1u;I?g(72vY%6-V@&*v)EEC?yY>A^#2zbiB{ASz0i5yFCa z;xsr3e%PS56!gU^^j@Q{w@N)*A?2m|*f-{)EW3FPZ}XIagd5WF5Gpc%_6=|<>h_#~ zqr&j3h9tB;n73B^KIG3&w)Mny1(X#T%?|ausf77j%{OHhttEeSKq~Sne3uM4@3xP| z+7Gq04xz72m_z!ussZyot{4vZT~x;XJe>I3S6|<1f>UrJjdZW0ZHW*2;iXkp4gNV9 zqVwEsq^NQRirT4^#EJ0p;HFEdQ~H0m=Jr9%euVM6`$*bo9ZK$eKK4Ej+W1_Bs*P^7 zAGiVqdlfaA{KcEnG8Yl>5^#ANG>S2H5_}aCi+T`l2%VUzX36RE`fSZrTlSW>9(6Wf zEX$_ZsQ-vhEis&+Eb#3$4UpuTxR==N2nyDCOzfgdJ1gsduo_LE-0QN31?ylbB2>^& zBEC-)VI(}l)SxTe=v02KMh{{ilbm`r8)6HoNKZt%S4pC$zo<qf7;PE zY8Rz^c(oR6=l!aQ?&nLZwB3J3M%ioc9u_}GDU47P9j`sl`4B-@Iexb8ZGYRf0FYdA z6k`jSXQj-8^a()c!w*@($7he=?a1e6UQJ857uMGy&Edw6J0{o)@7J|^53npqm@?+} zb)7ZlO!AZ?sKW=fu@R!RP$750AsTE9_0(TQQJ*0;-~wUMk_KFVRI-wojt%JPW6~P= zrc2}p7_0+~b_y589u-gWOMJysdZMX5Tl*8N%bad;<)c^)O`xUksrL$oRk=GgLmN~Y zpY_o-rSyq2AD&bDGo9cnK6oNQl-J+C?|0XC$ZF%Kt$v3>v8weS7V5*}YiIHDF!8qQ zF|56e6;u2)sfhR+GRPnqwkJwD-Js!vsKh!C#?!KEvL9+>!m7r=h_MP57|YvFg>T-$ zQ)e`-B0G0}>%?QxuYYK^zV(_Bj2iu>;%iwd`zN@HGtMoBbt55lAfezz3ySr5;J|pb zA*H$o1?uqM7)9u!pLptS|GJdoWL8ab9s{~d-G(C-`l8C&fJu;c9z}d7=c0$MYIRQjPS;fkxW6<_%HAMZZ?r3qUFA&ksnZYhtecv`(96ec3u} z2)WdM)65lmHdEY851I77+Bk~4^}#X++uNUun`|-2njq0dP_G()@sK4Idh!5#ShX_V zD)K#hp;As+<9Lgj2~TQ%*|#0WQLjr!G1)MNX2L}frJ~SYmbbG%S`Qr5eI=;jv@D1> zJVRRmeo>H5{``)&sUS_TT@5mbo7Tj=d}lg-rS&$~yb@uR+r)|3`P_$=wta~OHY;C-(v*D5L!DK2*Hvo6jK@kAs*1Iu`<$e=oD>gcuut}Q z<@s*a!6$ZEiU~8s;2*hqb20MeP~iAkB{I>`+3Z(a!`UY&{c}&+h%4x8t`Fm_ zv}DBjQ_bckwvVuO+l@%Z^aR}YFGm1o*Zg5juq`)XU|quz6y_&79~Q8fW~Ibxb* z0<|TWI}y(wXR^f3uD~)}J6j6LMH~o8c|;M|<>D{on%vXIlJaXqPIhZAm+HQ}Mm#Oj zFd_5LrLqCBsE#uF&#vpEpBMlwLr;9Wyx0H`uKDd8RXC*iwRv;F!3}UY!8IRO8O`z7 zlCi|j>@qy@=|{e%Y2@UhcKr|h-#(gcH`_k+tKTSR!`3LsK@2_?)M&i(IH)*ia(u0b z_Oz@@}lO`n5}c?LxcdNA>B_UbBP$Mc!9=Z&2<=0!*$b?JCas$$~D4n&?WEwon~=Ti>MKmm$80BegXe;Eh-v$8J{)~$39d)(mcIM|TSB76l-e@lYMQMEv*eGM~4+kt<~ z8T7|cg0WQkM{V+s3GY_aM}xi$Kk#Ac{rh9JvJKTo4|F#%BL4I|p3xAzM?BMPTeF0i zH05Bs>{f~GJIZaN3oTm1NfHLs%*lM5cz?sdV?Ajk>3F@rO(o&Y8@061B*nPx(`Y!I z6^--|cUfwR-GFLOW6;y!922PTK?f|7Wp1PE%AedK)C|`Ln+9rT_HZ1pcTdN(Ld8w@ z{)o$@j;-k9muQt-uvENV%dyaUnt#z9*i5mjQ-tH%J7}nk!~TU)J^U>;MNj2>#f&#h zgYrW5S5@D~IAqYc`UA3JQDCSo7bKc$QGh7E z%PSMRmWPY;Dfbx6-0RP7tT$_H`E@%h{MY11j)rSGvHt`Qb=1cK?$0xZ_<(OcJXdZ} z_iPOi=KmB*!LAuQOgXfKJ_vvfHF>{x`&YT5R;?Jq#(WIoNMm)tt>(D$J-Y_R!>oMTpKW<*nhSm-C*hbMB5fuBe8z8h;)E6-HaL{)*~0lC=z%b;$643MaD zd-J|Y0nP~|elryt#p_Zh?#-UQbB z6TxWacx4V#9$Gbd6ex7C+pgV;qm41=z9^@$r`SSbcs^GeM|8aO)|(fN@j9=k{pA|2 zm;LL~)LK0G5U}vyEx6{usFl^%wdB1+&Benz>tk?iCx^TGmdLi56hY>5)aw_&!+TqD z=+m^fIn&>qhHiiU$d>I$6`eOsMHDefz&S3&VLZr%EBUwt16kB2ag;TN-0isZRkJ)q zXZ?D?nAiy{Sv>X1PmMF2>bt%d-#GT&f&`I3A*yx&Pi9W#9JJz-eK=-nFauX`-#`Ar!CDc zzB5mi{<~etCqsbU{(!R|tcHF?CgKyVb5x!=2>mQvdk$q%`IK%JbKRyMKi_>=(@vy< zR;V3)Pq1)y_UsVzogDppo{i}>xc+ue>q*ofet()BjovAFt?~V%@%}lFQOB`|d~|(G z_NO0$nwqjqh#$HkV7We`3JxKMx_rd|rRk`^Z_{bnV7{O$zfGv03tdg6F%80?|9m1%s+ zl;L7IeI^uKt=OnobCUkBLj0nA|G;f;8BuEHQES^oH%KJebS32#_|`pMX;`I12`)D3 z6ZHDs#ekx?teOiYeE2d5wa`cluY9A(fERRb%Fh4`xE&`?jH^VaT_7dZp-ovvQ z`1uWZELyLN*aqq+Y_AHmy*t^nu;6>6YX0=md8OmSrc~UW8GdyYus4|>CW_FWEntN?Dc=$U!2j%GV_88 z5_Stl0)+mm-V`daZ-(4hdoOS|?Y8PC!(5dzeIm6Tn5+tsOvV9q(ikv>9BHwWS(cDn zdy_kRfx&jRBX$=*Q%o7FG+MvAi;#zMzs8BPm!I6ygE8+h#pmr$LJHjro>2m|i*WKQ z#??b@HwdGBq^N}}j+)NgxBIP9>@F$|`aknK2N!_@3M2Jio;TmD=7{NXFh%jVax|0 z8wW?&FmuMUQC?X{?d`uYrlRoqh8zxfr0(E#oh}fp;BJ61a*-kQl_*sR%TB5oc)HY{r z%D%A3w9xD+B_f6LKpbu(QyUEl-%+z7gvSQ$9fqYt!*BaV%l0M_Ebtk(I96)>&Q2bW z4!F51Ms@uX1&f+uXn96~gnGqVDE348b&s!zh#c4k&%W0L)szpO0@ zpSq|Rfq%JjaC0P7hI0lAaU!6Qd*b1!Zsk0{W`fv~qXK?tuOrROB)g5bSmV8~c2BN1 zHMxo6(gj8g-wm9pcNL~lLqfungW!!fB}_?Yf8Mwf%v(r0vK1YEc&wITL^SDSc3@xo zDn9;cmt_7{Qj6<&OxeTVCFF!F!|lKt$uy_!dt7ujowK$CTl)wrLROIT5`R@4UeVo> z6BaZyN&5F)e9j%5e@!m=_cK91iD7!&?V$WM%~7TB8KqK=7WIJGIPxShzp$(k)Dc-YrN3+ddikdQiGe;!>h()AGLEwxw1X~Z* z{n;B9X8o=1h;Gq8gzwXt~R2gZuOO9mk~Y$XK(ix3#FNwon^S<`*DW!__7u>{0!)*rZHYhUz1tvDH0;#)deAOll z1SAto-&cp@@4vq0+^ZtF*U*N|Qfe8n1Rl;49$rObyTULmy!bg_w` zMYTf9OpWBAhe^dgkAZhgZ*F~Onm$n(R`T=zqe-EmJO&!_%qoku-GnE35bnr>GG0JZ zr)azcCPm=&ukIj8C~p1}k*6QQgL#$hC(AM>;=`pi5c|W!!xK{BXMcpvb2cAwF^BtQPuCJIybO47=zW z(qq5`$fn0aPCSyIohgRHZMJBc&7X=AjPLFtyXR8TY6<#gj>VB7w5eqQvpZktgzvSAq`HG{xNVx#QhbpXs`eVw9p?iUBASb%R$o&7R2#- z>89e{p46r-{tlQkRs6BoOYnA%pE*fpaAQ)08+M3un#l*lA0Y@dK1-U6goFWBn;$Nt z7K?q2GL#aBEW5|!7HTE*i}fRcRUgVksqslbx^2aWPaBP-;VWe%E-~x_2~JFs?+aS+ zE*s@b+WoU~{?POrq`Vg7q<1`C9EQMP`FQXighwCsGTlaY>|2l_xha$;oSpz&nB z0xYViofVSrx3nCtHqZX@c-FnudNww(btq|N$gIx8WA_H(u14vD%Qh0%!QM`o7+lM7 zst0fkKWJfw_Z3%&#M^}iR^d7M)Q}yyYu+V+YkJ=NkSjnY!%T9E`G!5OC|s0mJl0c` zT*>jb`We`!{##cR(35ZzmKu=j!hbII!<)aZpx_Y=<;lW(;C&qYE-DD!_ki`9?AOUF z(`TC=rdU_1&sV!-)T7VQEKs{^FcpBKnGL>+-80;`)+@!Bk{90xW0%Ydq2EppaE$^m z$2K(%x1BHvmF!y(YBGGrf;a>1Cb|IQVD-DaW}=NDD)EQo0zz`i=^G@j99(Ow zA-OG8Cq?3C`$KL>ZiQ<-1Cz_*dQ9>1a*YJuTneTbmDkjwcomZDXy%}R5tTt?$Aww& z z9GC|>$ZhM|6vqUVS@T%NE^@vN*iKDgI+ze*ri@xeSst&|R3>6`10CH39a)F-Vr)Eb zl{T`AY&S~~R>|J^jIg#*hGc+Gz$6X!SL0s3G6$aKeR%LCj5sh#nWh<@Y-EOU z?@;&4BQB5`%&{N{q6B5u{Hhn}rEx=M0c zlMz^wzdsJXLb)1ZlTue?MTJ?c$`(KxmZAg6uc(d)$HUj8I|xzGu%{i6*$XLi$x))IGulU61~AEDe`V+Ydq5g77Z7alfzO zqrg8CA%+uY|NfO(T@0T<+?Wc>C}{{-4{xzDWJdf2nSkI!;y(4mzqDiSI2h95AncUk zm6c1ie+&8MfAF(A4AH)JIiI{u5s5NZmG1ENQv4xcJ zQB`z%EzK>`si>$o8NjE*md6&X@$I~+WYh7gQ~pKgLrs#gv;8srA-5phMVvW-qzUyH zJFZV?9eQyEF6^?xj-K?tK^&XWn`&QPmI>t33w5zsk;!O zk9=edtYCV)KpU+86YyAClGd?I(t-ib?hYwXu+{H@&FwhWo-Z>oL)TND9+X@sTVI80 zQWH{8oWy&)-x^vb8V`J#T68j%J32z?!Oa>@sV%=?K3$G3q_c17w)I=b91_EG?woie zZ)3{@*l0wlCH{Df4Z~O3VbAzE?|%}5%0lT7VfTz+$~WdeJnAhZa51rtjd zn+AJ{_(pC6xN(Ttk)@gP_{zJaLJK+^2>O9KSpSQ5mz`{>5i{HdY9YD%rPJ26?iL=+ z0Oa=}SnJFtF6`SxIX`E?-OEXI)M$cbVJnAsE((ZI>h*Nm?!SjflWhYrQE6C}$FB%e zzUlzx0vw7P9E*}I)@gOMv&v#K=T_RJ-mdVXA5Jf-{T<-_Gw(WFNUcisGHPOJ(!Hdd72ioD23r5x%ORq33 zyt#F!z0mk5pRpPwgCVnE<1tDi296X zECI$v@{aUHG|=jhS~VuRp*qOpv_z%q6;o42$wJoO zJDquEmsXW@ec>8n0X3q>!#sXRSoWD1Zi1I?^B$$!EPdvCM%&GeErK3l!-8MOEA$B2 zBMq}9`W~`!q5)Dsxg56VcqPJ-H&c&@AFv-5;E834MvV_fU^W|hzMWMgO5!IRL7$?M$kD1N%hkTnj-_IOKwGP`g~&iUCk z+k%65MNQv>Nv}t7O*j_FVh-&HpwIZmKRha}pizi{;^2%-{7e^M3z&09_hpKQEL225_-M(l0BOW(PSnDiYX0J6xYl`2e{~c`N@4EdWbnNCjKo%dTYO$HU2Iwv%n_+ z6*G*#2n5+$aDX1fAPHz`=sZ0<44vnqul=Cz4IFRSv9vGK>1)Xc3nvB7Z;VEtsBkev zo1Mg`R=RMT)T0z|p+eY$uWexb!uLF2q1Pw6Dsl2+UKH&x7>8n6M$T8WW?t2RHJ?ePoRs{%BjE+#b0y-vx4C8p5ak{p%p)v8ZG&v*Hm||8 zY^=Lp_)S+$#GwQ4AUYgwCuvRii5lsmu<~40$#4v&`yo@xhp}1HD>BJEZg}3XpP}`GMpKDz_Wq&)cwW#0bhKIGRr)I`6fX##zafa_XCKqm8+5S}= zJhkxIh3w55(l{~pXB6ll;-K0-Szf5$f);Q=hqb$Y74wMIAhpK4rd8ZF2;FAI zJq};|g1@qiU$-Sxef}5{L1sLFJtLjXbd!r~pxJ{)ph>i(W!-c zapbVZEQ$FAaLwsBgM>c9%!W9+YMz*Nkt)nS z_{XRYuUJ`?tTT0&3!`)Gu7b7Z!Bt}ww+O!iMx~I?PF4&}qa{bGS=ZJ)3UgcpuJDQ_ zd$gBIl?*EMM=~m7!|AMqSa_)NMnyY&e5<(_@%hm6Z$dy=XSm{&4;%ECt z|HRdVa}!mAXccBl)2?+%g$wl*8C~HO>uJ3{kBe1Is5F5nLRI!IP=#WlGRZO{l`9b} zB3Q0lM+L6#i{Gjev|E%aoit5Yn#kvhh%SOxuiHiHH6Iu(82kWJWF5OE3x3$GSI5eC zgeo*Tqe`-HGPn?lYj+NgdPhQ@tImCg3oZaF&vZ+y9BF*Di_rz~+Ia;5LxeR6VeU<6 zs#7zMmH|>&r)>WpjZXoMN^N4KbZ4kS#dn0mjUu^<$hA8Ml2^P0i!U_H{#lw`6-px$ zwF?B7V6Zfsk}$duUzD&{dyp{z8KA6t3XZ5`%it^BZm)N6MAw9J5Q+bF_ydrtY_6EO zV+ZAhVI_;Ws|drOt6)jM^1&5itC-79S1-m5KK${zC4L=Vl)-=@#I^xrkTQrF1kF0| z=R2g5AmGVkMT=IzRoykI;%lTKC5!C5ip;e;F4bFl<<1pO2*2{q+ktRpr)%dIHn=qF zL(;lN5{4mj9`wskGzqH?r9x2U?<7>LUl>NRNDD{xpy;4r30&RtF&9uM<;qi&RXbdy z6rkJ4GVJ(0;j3U?$6i&KDBoSE*yMW%$zow}3{?hoINHq78#Gu%ve=M7=5pn|mxlxGoGVWXZJv z00r|&L_t)M7sDz-*gM5fXbIoaFZ_EMMl7JxCdw_k?T83hx4OS1e?{sFOvK zHNdK@%Xe|vD^J0~l0|l7t-T(V7Tg2VRV?L3vJ+#IHK<>Pp&MS#oQz3aTfu&mF5^lpy3cVRXr4he zS2oHDg<=tq22O6IOb#FpoT;g)v8gfeNfvx5m(Q2LZ!CPdNC_o}+dy)KvcZM(_oFd! zMFmUj=7?wYbtg)${@>pDBPR}nQG5-`B-D(t!Egww+<_T&6z=yZI)V#aa+IBTW>eFU znKbG49IHRJBpU+;EMnsKvNLHmmEQc`(lwms^JdgQGlJ5z^_^7YO z@Kx8suo1HM>gLt!Y{>u2=L>m%W5tKYTEkb}?C*ny2Rk{gcnpe*1JB_-vd{{`!xv`q z>+SGWr|0jVjdA6v^$Yk~ChmPjR~lKX8eql6b`ZYm=rHTd#<0NER6i&%B<{12mWZ{= zfn_->UJ&@I+x`9I_Kc+1-DH+Hewe|%H0ZnH#)}+1JPW zMtSOMm%XXe=K1W{SGc@{4(mIuRQD3FObnu4bzI^sdi&=s%d*`r)v7sx96N+%&TDagVh7VNPy@sj^6>bnbM)dFi}QgG=97>fR6 z$@=a9S#`ilO$xFIxccFEiyR|M?+F))PC?$TFl6}Y@|as><<}Z)TM=vq`_T78H^?f@ zM`=;6#2HfPVKdkVjol#2G8VNuJ5sPe(=R!3NZc4p+e(*qq+oxLC0E?hb$2Saf(5QZ zj6{K~R-Bb4b_!y2)i22*DRJN{Whh-TKCm(uGYch7<&{+EL!w8B!3)YSUJB zXD0ztu$Ow4H%VEWwz5+Y=Yl|%16yIx-gjiRHGa2F1x07apdH9+qrWGKZtSoXlp`x? zqqBonP!(A>h9PVP)g$YsFodn3W5{CH?+jrps9p+LSH8rSCfZd?5_zx{RB2h))T}iB zvJ&~{elWz`3aTRO;-}bXYQu_!IJ|+X8s*3$>2=MDXCSH-REMnGBh8#8S!KZr83>(5 zRwHMnsdaE8VTF4!&@rhG?1-QA{3-LY)T{t2=(yBp4AtRIdU6j~jzBaks0p&zQD;?9 z#a)F6gqkBun1_)hNwpFo!faH*RZw$z>T9^i7NCe=u%{rOjH%~!=T}7fe>UND=OD>+f&4kc>cju&{fWQ8jgb~^od>5xu?Ql p3{pWiZx2Vgy-4yejNqw9(SL!pd~2-nS;YVV002ovPDHLkV1l045^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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%0A~4ozdwcx5Yd)8RVX>K)ICXB;4ijF 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

hKbcV3|?^$$Wrh(G2EeXuUf>6RiQ!Hle6 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notify.png b/app/src/main/res/drawable-xxhdpi/ic_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..6a62def1f0fc2b684ab32e40656a785c689b3321 GIT binary patch literal 1710 zcmV;f22uHmP)0Gm!;GWZl;t7Pk+k$e5i~4Qri8-CP@0Zt2a3870&mJgfkl+wm|jFtYIYNq zB_^FPvXsimVeI#A!(Q|6??3FlzJJdM`-Qn#>-*mKUEi$zum9Mk zOBGdAaVY?}5jX-o3v}xs4h-AA|?G+gZDVlD|LUcpnY0HJJjG_ z!N3F~X1oxS1A1iCUM}!HDa36O;_lC)9cu7Oiho|ag}7Q^yI^v1Mt!q=I{`EZaSsXF z@f&Mku^{}sC3cDsyPq=qCBS5gyAty)vHb;YwRX^D@}MAmvnBQlAr_!tnc8~V)65_5I;b(I5iei zMLlBRrC1dR#2CTT6GGf5%>s3koCtBB$9(cYCX(XpjQEgQkpt!<7pUa% zV5tRK39OI!a9U^u9v7~w7U(iTKpK~IR^)_h(chp;9n}JPpjx2%jAky7%tg2TNz22P zhU#Zt>?k^Lj%2EJE->1{*#yiBqGu!iL8IRGI7(R08m;F=P6 z*LgUDc88EmeYm#bn7Fq5Y7~pxrVP?Jj4%!g;2sK`wQ%OJ42T_k^9h@vd%sk#E>YhY zYbyb>dk+597T)j6%JrNUYxIcxME2$6W()US890aF@2kC@#dn*~Oou=# zaCv5tM916=bwGi$iKJb0VPIAf*Fbc9)`v*xtT4FODe>OwDbVd%$ggq`zpC*7DJ}C& zRoi?CyzO|7nOTUXd6YI-*Tl=zGFMK6y9xo^u77l<0*!VM2QA+M{jhWvh1dgm?xTV6 zBMF>aCOMu^u>x%g$gKswOywNIv$KN2=YFFnZV2u7A#Mb^dva{QhD-$-Xd(3;GXNeF z^3LGi39=v`x7@@1yTCoqlQSSjfuy-;2z!Ixv*gx0a;D~EoJSadQv&a&o}5AI1fT=n;G|0(b~H;5wdnFP4ST>%e(=@#Nap1eld_o2=MQv&op z3W&Lb_T#NRcp7V9iRC%6t*|2R^#jMUb1JuvWUtSo1Z-8w=Y)Qh1_6%%_W`{;ww5jr3zq8N^GP8Up>k6h4eQVg~k+^m3kwcW5~>) z{n)H$R52w`m&Er*6QH2Kn2au_fRpOZkWdHyG07*qoM6N<$ 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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�NkOghXp*Kic>JEO@)M|0jf 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 N002ovPDHLkV1fJ`?~T=mOyb007L%gem|4 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 zup

lGr?op` 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
RUYxdAnp0YBe3)X1_R7;? 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 z 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 UAL_ij#Q*>R07*qoM6N<$g7Aeevj6}9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/elephant_friend_empty.png b/app/src/main/res/drawable-xxxhdpi/elephant_friend_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..6da3375f177050fd429a06d732c89701bd7220e0 GIT binary patch literal 180584 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

@nDZ>s|U9 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 Xyl9d@4YzRf00000NkvXXu0mjf<{xa2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notify.png b/app/src/main/res/drawable-xxxhdpi/ic_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..d2a507d2be1053e1f7f91be37fa2c1d47de938fd GIT binary patch literal 2642 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 D`x6d; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/avatar_border.xml b/app/src/main/res/drawable/avatar_border.xml new file mode 100644 index 0000000..aca56e1 --- /dev/null +++ b/app/src/main/res/drawable/avatar_border.xml @@ -0,0 +1,6 @@ + + + + + + \ 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 @@ + + + + + + + + + +

?FPZOhiUQQ<@d9~-xt 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�%;mmY;A6ZucMX!}IjJ|eWabMu4L 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

Rk)c@jL;UMo=)J?)i`mJX-9h} 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

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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