diff --git a/app/build.gradle b/app/build.gradle index 36088c27..5d68379c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation "androidx.core:core-ktx:1.2.0-beta01" implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.fragment:fragment-ktx:1.1.0" implementation "androidx.browser:browser:1.0.0" implementation "androidx.recyclerview:recyclerview:1.0.0" implementation "androidx.exifinterface:exifinterface:1.0.0" 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 00000000..ca6e30ab --- /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/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea510482..44ba5455 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -111,7 +111,7 @@ android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> - + diff --git a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java deleted file mode 100644 index 0309d741..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java +++ /dev/null @@ -1,76 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky; - -import android.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 javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasAndroidInjector; - -public class FavouritesActivity extends BottomSheetActivity implements HasAndroidInjector { - - @Inject - public DispatchingAndroidInjector dispatchingAndroidInjector; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_favourites); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar bar = getSupportActionBar(); - if (bar != null) { - bar.setTitle(getString(R.string.title_favourites)); - bar.setDisplayHomeAsUpEnabled(true); - bar.setDisplayShowHomeEnabled(true); - } - - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.FAVOURITES); - 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/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 341a9a03..1c874c65 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -91,15 +91,16 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut private static final long DRAWER_ITEM_ADD_ACCOUNT = -13; private static final long DRAWER_ITEM_EDIT_PROFILE = 0; private static final long DRAWER_ITEM_FAVOURITES = 1; - private static final long DRAWER_ITEM_LISTS = 2; - private static final long DRAWER_ITEM_SEARCH = 3; - private static final long DRAWER_ITEM_SAVED_TOOT = 4; - private static final long DRAWER_ITEM_ACCOUNT_SETTINGS = 5; - private static final long DRAWER_ITEM_SETTINGS = 6; - private static final long DRAWER_ITEM_ABOUT = 7; - private static final long DRAWER_ITEM_LOG_OUT = 8; - private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 9; - private static final long DRAWER_ITEM_SCHEDULED_TOOT = 10; + private static final long DRAWER_ITEM_BOOKMARKS = 2; + private static final long DRAWER_ITEM_LISTS = 3; + private static final long DRAWER_ITEM_SEARCH = 4; + private static final long DRAWER_ITEM_SAVED_TOOT = 5; + private static final long DRAWER_ITEM_ACCOUNT_SETTINGS = 6; + private static final long DRAWER_ITEM_SETTINGS = 7; + private static final long DRAWER_ITEM_ABOUT = 8; + private static final long DRAWER_ITEM_LOG_OUT = 9; + private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 10; + private static final long DRAWER_ITEM_SCHEDULED_TOOT = 11; public static final String STATUS_URL = "statusUrl"; @Inject @@ -144,8 +145,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut 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' + - from our code as long 'account_id' + - from share shortcuts as String 'android.intent.extra.shortcut.ID' */ long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1); if(accountId == -1) { @@ -387,9 +388,10 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut } }); - List listItems = new ArrayList<>(10); + List listItems = new ArrayList<>(11); listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_EDIT_PROFILE).withName(R.string.action_edit_profile).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person)); listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_FAVOURITES).withName(R.string.action_view_favourites).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star)); + listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_BOOKMARKS).withName(R.string.action_view_bookmarks).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_bookmark)); listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list)); listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(R.string.action_search).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search)); listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SAVED_TOOT).withName(R.string.action_access_saved_toot).withSelectable(false).withIcon(R.drawable.ic_notebook).withIconTintingEnabled(true)); @@ -415,7 +417,10 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut Intent intent = new Intent(MainActivity.this, EditProfileActivity.class); startActivityWithSlideInAnimation(intent); } else if (drawerItemIdentifier == DRAWER_ITEM_FAVOURITES) { - Intent intent = new Intent(MainActivity.this, FavouritesActivity.class); + Intent intent = StatusListActivity.newFavouritesIntent(MainActivity.this); + startActivityWithSlideInAnimation(intent); + } else if (drawerItemIdentifier == DRAWER_ITEM_BOOKMARKS) { + Intent intent = StatusListActivity.newBookmarksIntent(MainActivity.this); startActivityWithSlideInAnimation(intent); } else if (drawerItemIdentifier == DRAWER_ITEM_SEARCH) { startActivityWithSlideInAnimation(SearchActivity.getIntent(this)); @@ -591,7 +596,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut .withName(R.string.action_view_follow_requests) .withSelectable(false) .withIcon(GoogleMaterial.Icon.gmd_person_add); - drawer.addItemAtPosition(followRequestsItem, 3); + drawer.addItemAtPosition(followRequestsItem, 4); } else if (!me.getLocked()) { drawer.removeItem(DRAWER_ITEM_FOLLOW_REQUESTS); } 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 00000000..56ea4d2f --- /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/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java index 915b835f..e8b484ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java @@ -71,7 +71,8 @@ public class TuskyApplication extends Application implements HasAndroidInjector 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_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, + AppDatabase.MIGRATION_19_20) .build(); accountManager = new AccountManager(appDatabase); serviceLocator = new ServiceLocator() { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 1d978975..370864b5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -64,6 +64,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private ImageButton replyButton; private SparkButton reblogButton; private SparkButton favouriteButton; + private SparkButton bookmarkButton; private ImageButton moreButton; protected MediaPreviewImageView[] mediaPreviews; private ImageView[] mediaOverlays; @@ -107,6 +108,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { 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); mediaPreviews = new MediaPreviewImageView[]{ @@ -348,6 +350,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { favouriteButton.setChecked(favourited); } + protected void setBookmarked(boolean bookmarked) { + bookmarkButton.setChecked(bookmarked); + } + private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) { if (TextUtils.isEmpty(previewUrl)) { Glide.with(imageView) @@ -582,6 +588,27 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { public void onEventAnimationStart(ImageView button, boolean buttonState) { } }); + + bookmarkButton.setEventListener(new SparkEventListener() { + @Override + public void onEvent(ImageView button, boolean buttonState) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBookmark(buttonState, position); + } + } + + @Override + public void onEventAnimationEnd(ImageView button, boolean buttonState) { + + } + + @Override + public void onEventAnimationStart(ImageView button, boolean buttonState) { + + } + }); + moreButton.setOnClickListener(v -> { int position = getAdapterPosition(); if (position != RecyclerView.NO_POSITION) { @@ -621,6 +648,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar); setReblogged(status.isReblogged()); setFavourited(status.isFavourited()); + setBookmarked(status.isBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.isSensitive(); if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) { @@ -690,6 +718,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { 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()), diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 6d3f9733..6404de5c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -26,6 +26,8 @@ class CacheUpdater @Inject constructor( 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 -> diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 32deb004..7bdc17e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -7,6 +7,7 @@ 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 UnfollowEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 754a22ea..a59df12b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -69,6 +69,7 @@ data class ConversationStatusEntity( val emojis: List, val favouritesCount: Int, val favourited: Boolean, + val bookmarked: Boolean, val sensitive: Boolean, val spoilerText: String, val attachments: ArrayList, @@ -148,6 +149,7 @@ data class ConversationStatusEntity( favouritesCount = favouritesCount, reblogged = false, favourited = favourited, + bookmarked = bookmarked, sensitive= sensitive, spoilerText = spoilerText, visibility = Status.Visibility.DIRECT, @@ -172,7 +174,7 @@ fun Account.toEntity() = fun Status.toEntity() = ConversationStatusEntity( id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content, - createdAt, emojis, favouritesCount, favourited, sensitive, + createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive, spoilerText, attachments, mentions, false, false, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 268fd5b7..becb537f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -83,6 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setCreatedAt(status.getCreatedAt()); setIsReply(status.getInReplyToId() != null); setFavourited(status.getFavourited()); + setBookmarked(status.getBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index b6eef435..f192cae7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -117,6 +117,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res 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) 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 index dd28b0cc..2cb9c2d9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -66,6 +66,24 @@ class ConversationsViewModel @Inject constructor( } + 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) } + .subscribe() + .addTo(disposables) + } + + } + fun voteInPoll(position: Int, choices: MutableList) { conversations.value?.getOrNull(position)?.let { conversation -> timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 122701c9..c912bdce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -187,6 +187,18 @@ class SearchViewModel @Inject constructor( .subscribe()) } + fun bookmark(status: Pair, isBookmarked: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isBookmarked).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + disposables.add(timelineCases.favourite(status.first, isBookmarked) + .onErrorReturnItem(status.first) + .subscribe()) + } + fun getAllAccountsOrderedByActive(): List { return accountManager.getAllAccountsOrderedByActive() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 2829e6b6..5a4670ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -94,6 +94,12 @@ class SearchStatusesFragment : SearchFragment + viewModel.bookmark(status, bookmark) + } + } + override fun onMore(view: View, position: Int) { (adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { more(it, view, position) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 37f7f9cd..d37df70a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -30,7 +30,7 @@ import androidx.annotation.NonNull; @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 19) + }, version = 20) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); @@ -310,4 +310,12 @@ public abstract class AppDatabase extends RoomDatabase { } }; + 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"); + } + }; + } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 51755dd9..80a1211a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -24,7 +24,7 @@ abstract class TimelineDao { @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.sensitive, +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, a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', @@ -77,6 +77,9 @@ AND 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)""") diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index 73192003..94111a95 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -41,6 +41,7 @@ data class TimelineStatusEntity( val reblogsCount: Int, val favouritesCount: Int, val reblogged: Boolean, + val bookmarked: Boolean, val favourited: Boolean, val sensitive: Boolean, val spoilerText: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index c107e0c6..75d6b446 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -60,10 +60,10 @@ abstract class ActivitiesModule { abstract fun contributesViewThreadActivity(): ViewThreadActivity @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) - abstract fun contributesFavouritesActivity(): FavouritesActivity + abstract fun contributesStatusListActivity(): StatusListActivity @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) - abstract fun contribtutesSearchAvtivity(): SearchActivity + abstract fun contributesSearchAvtivity(): SearchActivity @ContributesAndroidInjector abstract fun contributesAboutActivity(): AboutActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 0563f854..2ccf0dcb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -35,6 +35,7 @@ data class Status( @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, diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 4150a6ee..4d5afd1e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -55,6 +55,7 @@ import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.appstore.BlockEvent; +import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; @@ -309,6 +310,16 @@ public class NotificationsFragment extends SFragment implements 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; @@ -365,6 +376,8 @@ public class NotificationsFragment extends SFragment implements .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 BlockEvent) { @@ -463,6 +476,41 @@ public class NotificationsFragment extends SFragment implements 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.isExpanded()); + + 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(); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 2431bc92..b3c42f87 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -49,6 +49,7 @@ import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.adapter.TimelineAdapter; import com.keylesspalace.tusky.appstore.BlockEvent; +import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.DomainMuteEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; @@ -128,7 +129,8 @@ public class TimelineFragment extends SFragment implements USER_PINNED, USER_WITH_REPLIES, FAVOURITES, - LIST + LIST, + BOOKMARKS } private enum FetchEnd { @@ -492,6 +494,9 @@ public class TimelineFragment extends SFragment implements } 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(); @@ -630,6 +635,38 @@ public class TimelineFragment extends SFragment implements updateAdapter(); } + @Override + public void onBookmark(final boolean bookmark, final int position) { + final Status status = statuses.get(position).asRight(); + + timelineCases.bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), + (err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) + ); + } + + private void setBookmarkForStatus(int position, Status status, boolean bookmark) { + status.setBookmarked(bookmark); + + if (status.getReblog() != null) { + status.getReblog().setBookmarked(bookmark); + } + + Pair actual = + findStatusAndPosition(position, status); + if (actual == null) return; + + StatusViewData newViewData = new StatusViewData + .Builder(actual.first) + .setBookmarked(bookmark) + .createStatusViewData(); + statuses.setPairedItem(actual.second, newViewData); + updateAdapter(); + } + public void onVoteInPoll(int position, @NonNull List choices) { final Status status = statuses.get(position).asRight(); @@ -917,7 +954,7 @@ public class TimelineFragment extends SFragment implements } private boolean actionButtonPresent() { - return kind != Kind.TAG && kind != Kind.FAVOURITES && + return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && getActivity() instanceof ActionButtonActivity; } @@ -950,6 +987,8 @@ public class TimelineFragment extends SFragment implements return api.accountStatuses(tagOrId, 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(tagOrId, fromId, uptoId, LOAD_AT_ONCE); } @@ -1095,11 +1134,8 @@ public class TimelineFragment extends SFragment implements } private void updateBottomLoadingState(FetchEnd fetchEnd) { - switch (fetchEnd) { - case BOTTOM: { - bottomLoading = false; - break; - } + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false; } } @@ -1223,8 +1259,8 @@ public class TimelineFragment extends SFragment implements private final Function1> statusLifter = Either.Right::new; - private @Nullable - Pair + @Nullable + private Pair findStatusAndPosition(int position, Status status) { StatusViewData.Concrete statusToUpdate; int positionToUpdate; @@ -1260,6 +1296,13 @@ public class TimelineFragment extends SFragment implements 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: diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index b047064c..bd8494a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -45,6 +45,7 @@ import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.adapter.ThreadAdapter; import com.keylesspalace.tusky.appstore.BlockEvent; +import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; @@ -186,6 +187,8 @@ public final class ViewThreadFragment extends SFragment implements 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 StatusComposedEvent) { @@ -239,7 +242,7 @@ public final class ViewThreadFragment extends SFragment implements .as(autoDisposable(from(this))) .subscribe( (newStatus) -> updateStatus(position, newStatus), - (t) -> Log.d(getClass().getSimpleName(), + (t) -> Log.d(TAG, "Failed to reblog status: " + status.getId(), t) ); } @@ -253,11 +256,25 @@ public final class ViewThreadFragment extends SFragment implements .as(autoDisposable(from(this))) .subscribe( (newStatus) -> updateStatus(position, newStatus), - (t) -> Log.d(getClass().getSimpleName(), + (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()) { @@ -267,6 +284,7 @@ public final class ViewThreadFragment extends SFragment implements .setReblogged(actionableStatus.getReblogged()) .setReblogsCount(actionableStatus.getReblogsCount()) .setFavourited(actionableStatus.getFavourited()) + .setBookmarked(actionableStatus.getBookmarked()) .setFavouritesCount(actionableStatus.getFavouritesCount()) .createStatusViewData(); statuses.setPairedItem(position, viewData); @@ -621,6 +639,28 @@ public final class ViewThreadFragment extends SFragment implements 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; diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index ec7bcc21..ec37680c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -26,6 +26,7 @@ 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); diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 01813a3c..fca1776b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -180,6 +180,16 @@ interface MastodonApi { @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 @@ -343,6 +353,13 @@ interface MastodonApi { @Query("limit") limit: Int? ): Call> + @GET("api/v1/bookmarks") + 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? diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index eb59660b..ce9dc7a9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -35,6 +35,7 @@ import java.lang.IllegalStateException interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single + fun bookmark(status: Status, bookmark: Boolean): Single fun mute(id: String) fun block(id: String) fun delete(id: String): Single @@ -80,6 +81,19 @@ class TimelineCasesImpl( } } + 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 mute(id: String) { val call = mastodonApi.muteAccount(id) call.enqueue(object : Callback { diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index d32cf6b2..7cdc9ac9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -221,6 +221,7 @@ class TimelineRepositoryImpl( favouritesCount = status.favouritesCount, reblogged = status.reblogged, favourited = status.favourited, + bookmarked = status.bookmarked, sensitive = status.sensitive, spoilerText = status.spoilerText!!, visibility = status.visibility!!, @@ -247,6 +248,7 @@ class TimelineRepositoryImpl( favouritesCount = 0, reblogged = false, favourited = false, + bookmarked = false, sensitive = false, spoilerText = "", visibility = status.visibility!!, @@ -272,6 +274,7 @@ class TimelineRepositoryImpl( favouritesCount = status.favouritesCount, reblogged = status.reblogged, favourited = status.favourited, + bookmarked = status.bookmarked, sensitive = status.sensitive, spoilerText = status.spoilerText!!, visibility = status.visibility!!, @@ -341,6 +344,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { favouritesCount = 0, reblogged = false, favourited = false, + bookmarked = false, sensitive = false, spoilerText = null, visibility = null, @@ -371,6 +375,7 @@ fun Status.toEntity(timelineUserId: Long, favouritesCount = actionable.favouritesCount, reblogged = actionable.reblogged, favourited = actionable.favourited, + bookmarked = actionable.bookmarked, sensitive = actionable.sensitive, spoilerText = actionable.spoilerText, visibility = actionable.visibility, diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index ddd9b29c..8373d2bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -56,6 +56,7 @@ class ListStatusAccessibilityDelegate( 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, @@ -95,6 +96,8 @@ class ListStatusAccessibilityDelegate( } 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 -> { @@ -272,6 +275,14 @@ class ListStatusAccessibilityDelegate( 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)) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 7997ce86..50b75ba5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -42,6 +42,7 @@ public final class ViewDataUtils { .setFavouritesCount(visibleStatus.getFavouritesCount()) .setInReplyToId(visibleStatus.getInReplyToId()) .setFavourited(visibleStatus.getFavourited()) + .setBookmarked(visibleStatus.getBookmarked()) .setReblogged(visibleStatus.getReblogged()) .setIsExpanded(alwaysOpenSpoiler) .setIsShowingSensitiveContent(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 index 365e5c8a..b43c7628 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -42,8 +42,7 @@ import java.util.Objects; public abstract class StatusViewData { - private StatusViewData() { - } + private StatusViewData() { } public abstract long getViewDataId(); @@ -57,6 +56,7 @@ public abstract class StatusViewData { private final Spanned content; final boolean reblogged; final boolean favourited; + final boolean bookmarked; @Nullable private final String spoilerText; private final Status.Visibility visibility; @@ -92,7 +92,7 @@ public abstract class StatusViewData { private final PollViewData poll; private final boolean isBot; - public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, + 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, @@ -114,6 +114,7 @@ public abstract class StatusViewData { } this.reblogged = reblogged; this.favourited = favourited; + this.bookmarked = bookmarked; this.visibility = visibility; this.attachments = attachments; this.rebloggedByUsername = rebloggedByUsername; @@ -156,6 +157,10 @@ public abstract class StatusViewData { return favourited; } + public boolean isBookmarked() { + return bookmarked; + } + @Nullable public String getSpoilerText() { return spoilerText; @@ -288,6 +293,7 @@ public abstract class StatusViewData { Concrete concrete = (Concrete) o; return reblogged == concrete.reblogged && favourited == concrete.favourited && + bookmarked == concrete.bookmarked && isSensitive == concrete.isSensitive && isExpanded == concrete.isExpanded && isShowingContent == concrete.isShowingContent && @@ -394,6 +400,7 @@ public abstract class StatusViewData { private Spanned content; private boolean reblogged; private boolean favourited; + private boolean bookmarked; private String spoilerText; private Status.Visibility visibility; private List attachments; @@ -429,6 +436,7 @@ public abstract class StatusViewData { 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); @@ -477,6 +485,11 @@ public abstract class StatusViewData { return this; } + public Builder setBookmarked(boolean bookmarked) { + this.bookmarked = bookmarked; + return this; + } + public Builder setSpoilerText(String spoilerText) { this.spoilerText = spoilerText; return this; @@ -626,8 +639,8 @@ public abstract class StatusViewData { if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if (this.createdAt == null) createdAt = new Date(); - return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility, - attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, + return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, + visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot); 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 00000000..803bca92 --- /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 00000000..217b78ba --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_active_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_favourites.xml b/app/src/main/res/layout/activity_statuslist.xml similarity index 85% rename from app/src/main/res/layout/activity_favourites.xml rename to app/src/main/res/layout/activity_statuslist.xml index e27b785f..7e4f7cb2 100644 --- a/app/src/main/res/layout/activity_favourites.xml +++ b/app/src/main/res/layout/activity_statuslist.xml @@ -5,7 +5,7 @@ android:id="@+id/activity_view_thread" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.keylesspalace.tusky.FavouritesActivity"> + tools:context="com.keylesspalace.tusky.StatusListActivity"> diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml index f0635ba1..55b0dd0d 100644 --- a/app/src/main/res/layout/item_conversation.xml +++ b/app/src/main/res/layout/item_conversation.xml @@ -446,7 +446,7 @@ android:clipToPadding="false" android:contentDescription="@string/action_favourite" android:padding="4dp" - app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" app:layout_constraintStart_toEndOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply" sparkbutton:activeImage="?attr/status_favourite_active_drawable" @@ -455,6 +455,23 @@ sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 167579df..7027a49e 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -451,7 +451,7 @@ android:contentDescription="@string/action_favourite" android:importantForAccessibility="no" android:padding="4dp" - app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" app:layout_constraintStart_toEndOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset" sparkbutton:activeImage="?attr/status_favourite_active_drawable" @@ -460,6 +460,23 @@ sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index 6cdc1a40..709289b8 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -541,7 +541,7 @@ android:contentDescription="@string/action_favourite" android:importantForAccessibility="no" android:padding="4dp" - app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" app:layout_constraintStart_toEndOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset" sparkbutton:activeImage="?attr/status_favourite_active_drawable" @@ -550,6 +550,23 @@ sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + diff --git a/app/src/main/res/values/actions.xml b/app/src/main/res/values/actions.xml index 72d0d958..a3a3168f 100644 --- a/app/src/main/res/values/actions.xml +++ b/app/src/main/res/values/actions.xml @@ -4,6 +4,8 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 37809044..92770f4c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,8 @@ #56a7e1 #ca8f04 #fab207 + #19a341 + #25d069 #8f000000 #44000000 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index cfff6e18..03704a8f 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -107,8 +107,8 @@ - - %1$s; %2$s; %3$s, %13$s %4$s, %5$s; %6$s, %7$s, %8$s, %9$s; %10$s, %11$s, %12$s + + %1$s; %2$s; %3$s, %14$s %4$s, %5$s; %6$s, %7$s, %8$s, %9$s, %10$s; %11$s, %12$s, %13$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d55c68c7..357695da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ Follows Followers Favorites + Bookmarks Muted users Blocked users Hidden domains @@ -67,6 +68,7 @@ Boost Remove boost Favorite + Bookmark Remove favorite More Compose @@ -91,6 +93,7 @@ Preferences Account Preferences Favorites + Bookmarks Muted users Blocked users Hidden domains @@ -443,6 +446,9 @@ Favorited + + Bookmarked + Public diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index da80f784..d3c54f0e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -81,6 +81,7 @@ class BottomSheetActivityTest { false, false, false, + false, "", Status.Visibility.PUBLIC, ArrayList(), diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index b7e95dc1..b766cd2f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -191,6 +191,7 @@ class FilterTest { favouritesCount = 0, reblogged = false, favourited = false, + bookmarked = false, sensitive = false, spoilerText = spoilerText, visibility = Status.Visibility.PUBLIC, diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index e95ee68e..b55be090 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -307,6 +307,7 @@ class TimelineRepositoryTest { spoilerText = "", reblogged = true, favourited = false, + bookmarked = false, attachments = ArrayList(), mentions = arrayOf(), application = null,