* add bookmarks to timelines

* add Bookmarks to main menu

* cleanup

* handle BookmarkEvent

* fix tests

* fix bookmark handling in NotificationsFragment

* add bookmark accessibility actions
This commit is contained in:
Konrad Pozniak 2019-11-19 10:15:32 +01:00 committed by GitHub
parent d6ec5ca8d3
commit d9694df0c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1235 additions and 122 deletions

View file

@ -105,6 +105,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.2.0-beta01" implementation "androidx.core:core-ktx:1.2.0-beta01"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.1.0"
implementation "androidx.browser:browser:1.0.0" implementation "androidx.browser:browser:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.0.0" implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.exifinterface:exifinterface:1.0.0" implementation "androidx.exifinterface:exifinterface:1.0.0"

View file

@ -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')"
]
}
}

View file

@ -111,7 +111,7 @@
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
<activity android:name=".EditProfileActivity" /> <activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" /> <activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" /> <activity android:name=".StatusListActivity" />
<activity android:name=".AccountListActivity" /> <activity android:name=".AccountListActivity" />
<activity android:name=".AboutActivity" /> <activity android:name=".AboutActivity" />
<activity android:name=".TabPreferenceActivity" /> <activity android:name=".TabPreferenceActivity" />

View file

@ -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 <http://www.gnu.org/licenses>. */
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<Object> 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<Object> androidInjector() {
return dispatchingAndroidInjector;
}
}

View file

@ -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_ADD_ACCOUNT = -13;
private static final long DRAWER_ITEM_EDIT_PROFILE = 0; private static final long DRAWER_ITEM_EDIT_PROFILE = 0;
private static final long DRAWER_ITEM_FAVOURITES = 1; private static final long DRAWER_ITEM_FAVOURITES = 1;
private static final long DRAWER_ITEM_LISTS = 2; private static final long DRAWER_ITEM_BOOKMARKS = 2;
private static final long DRAWER_ITEM_SEARCH = 3; private static final long DRAWER_ITEM_LISTS = 3;
private static final long DRAWER_ITEM_SAVED_TOOT = 4; private static final long DRAWER_ITEM_SEARCH = 4;
private static final long DRAWER_ITEM_ACCOUNT_SETTINGS = 5; private static final long DRAWER_ITEM_SAVED_TOOT = 5;
private static final long DRAWER_ITEM_SETTINGS = 6; private static final long DRAWER_ITEM_ACCOUNT_SETTINGS = 6;
private static final long DRAWER_ITEM_ABOUT = 7; private static final long DRAWER_ITEM_SETTINGS = 7;
private static final long DRAWER_ITEM_LOG_OUT = 8; private static final long DRAWER_ITEM_ABOUT = 8;
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 9; private static final long DRAWER_ITEM_LOG_OUT = 9;
private static final long DRAWER_ITEM_SCHEDULED_TOOT = 10; 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"; public static final String STATUS_URL = "statusUrl";
@Inject @Inject
@ -144,8 +145,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
if (intent != null) { if (intent != null) {
/** there are two possibilities the accountId can be passed to MainActivity: /** there are two possibilities the accountId can be passed to MainActivity:
- from our code as long 'account_id' - from our code as long 'account_id'
- from share shortcuts as String 'android.intent.extra.shortcut.ID' - from share shortcuts as String 'android.intent.extra.shortcut.ID'
*/ */
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1); long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
if(accountId == -1) { if(accountId == -1) {
@ -387,9 +388,10 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
} }
}); });
List<IDrawerItem> listItems = new ArrayList<>(10); List<IDrawerItem> 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_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_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_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_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)); 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); Intent intent = new Intent(MainActivity.this, EditProfileActivity.class);
startActivityWithSlideInAnimation(intent); startActivityWithSlideInAnimation(intent);
} else if (drawerItemIdentifier == DRAWER_ITEM_FAVOURITES) { } 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); startActivityWithSlideInAnimation(intent);
} else if (drawerItemIdentifier == DRAWER_ITEM_SEARCH) { } else if (drawerItemIdentifier == DRAWER_ITEM_SEARCH) {
startActivityWithSlideInAnimation(SearchActivity.getIntent(this)); startActivityWithSlideInAnimation(SearchActivity.getIntent(this));
@ -591,7 +596,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
.withName(R.string.action_view_follow_requests) .withName(R.string.action_view_follow_requests)
.withSelectable(false) .withSelectable(false)
.withIcon(GoogleMaterial.Icon.gmd_person_add); .withIcon(GoogleMaterial.Icon.gmd_person_add);
drawer.addItemAtPosition(followRequestsItem, 3); drawer.addItemAtPosition(followRequestsItem, 4);
} else if (!me.getLocked()) { } else if (!me.getLocked()) {
drawer.removeItem(DRAWER_ITEM_FOLLOW_REQUESTS); drawer.removeItem(DRAWER_ITEM_FOLLOW_REQUESTS);
} }

View file

@ -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 <https://www.gnu.org/licenses>. */
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<Any>
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)
}
}
}

View file

@ -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_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, 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_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(); .build();
accountManager = new AccountManager(appDatabase); accountManager = new AccountManager(appDatabase);
serviceLocator = new ServiceLocator() { serviceLocator = new ServiceLocator() {

View file

@ -64,6 +64,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private ImageButton replyButton; private ImageButton replyButton;
private SparkButton reblogButton; private SparkButton reblogButton;
private SparkButton favouriteButton; private SparkButton favouriteButton;
private SparkButton bookmarkButton;
private ImageButton moreButton; private ImageButton moreButton;
protected MediaPreviewImageView[] mediaPreviews; protected MediaPreviewImageView[] mediaPreviews;
private ImageView[] mediaOverlays; private ImageView[] mediaOverlays;
@ -107,6 +108,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
replyButton = itemView.findViewById(R.id.status_reply); replyButton = itemView.findViewById(R.id.status_reply);
reblogButton = itemView.findViewById(R.id.status_inset); reblogButton = itemView.findViewById(R.id.status_inset);
favouriteButton = itemView.findViewById(R.id.status_favourite); favouriteButton = itemView.findViewById(R.id.status_favourite);
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
moreButton = itemView.findViewById(R.id.status_more); moreButton = itemView.findViewById(R.id.status_more);
mediaPreviews = new MediaPreviewImageView[]{ mediaPreviews = new MediaPreviewImageView[]{
@ -348,6 +350,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
favouriteButton.setChecked(favourited); favouriteButton.setChecked(favourited);
} }
protected void setBookmarked(boolean bookmarked) {
bookmarkButton.setChecked(bookmarked);
}
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) { private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) {
if (TextUtils.isEmpty(previewUrl)) { if (TextUtils.isEmpty(previewUrl)) {
Glide.with(imageView) Glide.with(imageView)
@ -582,6 +588,27 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public void onEventAnimationStart(ImageView button, boolean buttonState) { 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 -> { moreButton.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { 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); setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar);
setReblogged(status.isReblogged()); setReblogged(status.isReblogged());
setFavourited(status.isFavourited()); setFavourited(status.isFavourited());
setBookmarked(status.isBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive(); boolean sensitive = status.isSensitive();
if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) { if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) {
@ -690,6 +718,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
status.getNickname(), status.getNickname(),
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "", status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
status.isFavourited() ? context.getString(R.string.description_status_favourited) : "", status.isFavourited() ? context.getString(R.string.description_status_favourited) : "",
status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
getMediaDescription(context, status), getMediaDescription(context, status),
getVisibilityDescription(context, status.getVisibility()), getVisibilityDescription(context, status.getVisibility()),
getFavsText(context, status.getFavouritesCount()), getFavsText(context, status.getFavouritesCount()),

View file

@ -26,6 +26,8 @@ class CacheUpdater @Inject constructor(
timelineDao.setFavourited(accountId, event.statusId, event.favourite) timelineDao.setFavourited(accountId, event.statusId, event.favourite)
is ReblogEvent -> is ReblogEvent ->
timelineDao.setReblogged(accountId, event.statusId, event.reblog) timelineDao.setReblogged(accountId, event.statusId, event.reblog)
is BookmarkEvent ->
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark )
is UnfollowEvent -> is UnfollowEvent ->
timelineDao.removeAllByUser(accountId, event.accountId) timelineDao.removeAllByUser(accountId, event.accountId)
is StatusDeletedEvent -> is StatusDeletedEvent ->

View file

@ -7,6 +7,7 @@ import com.keylesspalace.tusky.entity.Status
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
data class ReblogEvent(val statusId: String, val reblog: 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 UnfollowEvent(val accountId: String) : Dispatchable
data class BlockEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable
data class MuteEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable

View file

@ -69,6 +69,7 @@ data class ConversationStatusEntity(
val emojis: List<Emoji>, val emojis: List<Emoji>,
val favouritesCount: Int, val favouritesCount: Int,
val favourited: Boolean, val favourited: Boolean,
val bookmarked: Boolean,
val sensitive: Boolean, val sensitive: Boolean,
val spoilerText: String, val spoilerText: String,
val attachments: ArrayList<Attachment>, val attachments: ArrayList<Attachment>,
@ -148,6 +149,7 @@ data class ConversationStatusEntity(
favouritesCount = favouritesCount, favouritesCount = favouritesCount,
reblogged = false, reblogged = false,
favourited = favourited, favourited = favourited,
bookmarked = bookmarked,
sensitive= sensitive, sensitive= sensitive,
spoilerText = spoilerText, spoilerText = spoilerText,
visibility = Status.Visibility.DIRECT, visibility = Status.Visibility.DIRECT,
@ -172,7 +174,7 @@ fun Account.toEntity() =
fun Status.toEntity() = fun Status.toEntity() =
ConversationStatusEntity( ConversationStatusEntity(
id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content, id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content,
createdAt, emojis, favouritesCount, favourited, sensitive, createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive,
spoilerText, attachments, mentions, spoilerText, attachments, mentions,
false, false,
false, false,

View file

@ -83,6 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setCreatedAt(status.getCreatedAt()); setCreatedAt(status.getCreatedAt());
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
setFavourited(status.getFavourited()); setFavourited(status.getFavourited());
setBookmarked(status.getBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.getSensitive(); boolean sensitive = status.getSensitive();
if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) { if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) {

View file

@ -117,6 +117,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
viewModel.favourite(favourite, position) viewModel.favourite(favourite, position)
} }
override fun onBookmark(favourite: Boolean, position: Int) {
viewModel.bookmark(favourite, position)
}
override fun onMore(view: View, position: Int) { override fun onMore(view: View, position: Int) {
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
more(it.toStatus(), view, position) more(it.toStatus(), view, position)

View file

@ -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<Int>) { fun voteInPoll(position: Int, choices: MutableList<Int>) {
conversations.value?.getOrNull(position)?.let { conversation -> conversations.value?.getOrNull(position)?.let { conversation ->
timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices) timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices)

View file

@ -187,6 +187,18 @@ class SearchViewModel @Inject constructor(
.subscribe()) .subscribe())
} }
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, 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<AccountEntity> { fun getAllAccountsOrderedByActive(): List<AccountEntity> {
return accountManager.getAllAccountsOrderedByActive() return accountManager.getAllAccountsOrderedByActive()
} }

View file

@ -94,6 +94,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
} }
override fun onBookmark(bookmark: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status ->
viewModel.bookmark(status, bookmark)
}
}
override fun onMore(view: View, position: Int) { override fun onMore(view: View, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { (adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let {
more(it, view, position) more(it, view, position)

View file

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 19) }, version = 20)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); 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");
}
};
} }

View file

@ -24,7 +24,7 @@ abstract class TimelineDao {
@Query(""" @Query("""
SELECT s.serverId, s.url, s.timelineUserId, SELECT s.serverId, s.url, s.timelineUserId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, 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.spoilerText, s.visibility, s.mentions, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll, s.content, s.attachments, s.poll,
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
@ -77,6 +77,9 @@ AND
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
abstract fun setFavourited(accountId: Long, statusId: String, favourited: Boolean) 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 @Query("""UPDATE TimelineStatusEntity SET reblogged = :reblogged
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")

View file

@ -41,6 +41,7 @@ data class TimelineStatusEntity(
val reblogsCount: Int, val reblogsCount: Int,
val favouritesCount: Int, val favouritesCount: Int,
val reblogged: Boolean, val reblogged: Boolean,
val bookmarked: Boolean,
val favourited: Boolean, val favourited: Boolean,
val sensitive: Boolean, val sensitive: Boolean,
val spoilerText: String?, val spoilerText: String?,

View file

@ -60,10 +60,10 @@ abstract class ActivitiesModule {
abstract fun contributesViewThreadActivity(): ViewThreadActivity abstract fun contributesViewThreadActivity(): ViewThreadActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) @ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesFavouritesActivity(): FavouritesActivity abstract fun contributesStatusListActivity(): StatusListActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) @ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contribtutesSearchAvtivity(): SearchActivity abstract fun contributesSearchAvtivity(): SearchActivity
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun contributesAboutActivity(): AboutActivity abstract fun contributesAboutActivity(): AboutActivity

View file

@ -35,6 +35,7 @@ data class Status(
@SerializedName("favourites_count") val favouritesCount: Int, @SerializedName("favourites_count") val favouritesCount: Int,
var reblogged: Boolean, var reblogged: Boolean,
var favourited: Boolean, var favourited: Boolean,
var bookmarked: Boolean,
var sensitive: Boolean, var sensitive: Boolean,
@SerializedName("spoiler_text") val spoilerText: String, @SerializedName("spoiler_text") val spoilerText: String,
val visibility: Visibility, val visibility: Visibility,

View file

@ -55,6 +55,7 @@ import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.adapter.NotificationsAdapter;
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.BlockEvent;
import com.keylesspalace.tusky.appstore.BookmarkEvent;
import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.FavoriteEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
@ -309,6 +310,16 @@ public class NotificationsFragment extends SFragment implements
event.getFavourite()); event.getFavourite());
} }
private void handleBookmarkEvent(BookmarkEvent event) {
Pair<Integer, Notification> posAndNotification =
findReplyPosition(event.getStatusId());
if (posAndNotification == null) return;
//noinspection ConstantConditions
setBookmarkForStatus(posAndNotification.first,
posAndNotification.second.getStatus(),
event.getBookmark());
}
private void handleReblogEvent(ReblogEvent event) { private void handleReblogEvent(ReblogEvent event) {
Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId()); Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId());
if (posAndNotification == null) return; if (posAndNotification == null) return;
@ -365,6 +376,8 @@ public class NotificationsFragment extends SFragment implements
.subscribe(event -> { .subscribe(event -> {
if (event instanceof FavoriteEvent) { if (event instanceof FavoriteEvent) {
handleFavEvent((FavoriteEvent) event); handleFavEvent((FavoriteEvent) event);
} else if (event instanceof BookmarkEvent) {
handleBookmarkEvent((BookmarkEvent) event);
} else if (event instanceof ReblogEvent) { } else if (event instanceof ReblogEvent) {
handleReblogEvent((ReblogEvent) event); handleReblogEvent((ReblogEvent) event);
} else if (event instanceof BlockEvent) { } else if (event instanceof BlockEvent) {
@ -463,6 +476,41 @@ public class NotificationsFragment extends SFragment implements
updateAdapter(); 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<Integer> choices) { public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
final Notification notification = notifications.get(position).asRight(); final Notification notification = notifications.get(position).asRight();
final Status status = notification.getStatus(); final Status status = notification.getStatus();

View file

@ -49,6 +49,7 @@ import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
import com.keylesspalace.tusky.adapter.TimelineAdapter; import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.BlockEvent;
import com.keylesspalace.tusky.appstore.BookmarkEvent;
import com.keylesspalace.tusky.appstore.DomainMuteEvent; import com.keylesspalace.tusky.appstore.DomainMuteEvent;
import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.FavoriteEvent;
@ -128,7 +129,8 @@ public class TimelineFragment extends SFragment implements
USER_PINNED, USER_PINNED,
USER_WITH_REPLIES, USER_WITH_REPLIES,
FAVOURITES, FAVOURITES,
LIST LIST,
BOOKMARKS
} }
private enum FetchEnd { private enum FetchEnd {
@ -492,6 +494,9 @@ public class TimelineFragment extends SFragment implements
} else if (event instanceof ReblogEvent) { } else if (event instanceof ReblogEvent) {
ReblogEvent reblogEvent = (ReblogEvent) event; ReblogEvent reblogEvent = (ReblogEvent) event;
handleReblogEvent(reblogEvent); handleReblogEvent(reblogEvent);
} else if (event instanceof BookmarkEvent) {
BookmarkEvent bookmarkEvent = (BookmarkEvent) event;
handleBookmarkEvent(bookmarkEvent);
} else if (event instanceof UnfollowEvent) { } else if (event instanceof UnfollowEvent) {
if (kind == Kind.HOME) { if (kind == Kind.HOME) {
String id = ((UnfollowEvent) event).getAccountId(); String id = ((UnfollowEvent) event).getAccountId();
@ -630,6 +635,38 @@ public class TimelineFragment extends SFragment implements
updateAdapter(); 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<StatusViewData.Concrete, Integer> 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<Integer> choices) { public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
final Status status = statuses.get(position).asRight(); final Status status = statuses.get(position).asRight();
@ -917,7 +954,7 @@ public class TimelineFragment extends SFragment implements
} }
private boolean actionButtonPresent() { private boolean actionButtonPresent() {
return kind != Kind.TAG && kind != Kind.FAVOURITES && return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS &&
getActivity() instanceof ActionButtonActivity; 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); return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE, null, null, null);
case FAVOURITES: case FAVOURITES:
return api.favourites(fromId, uptoId, LOAD_AT_ONCE); return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
case BOOKMARKS:
return api.bookmarks(fromId, uptoId, LOAD_AT_ONCE);
case LIST: case LIST:
return api.listTimeline(tagOrId, fromId, uptoId, LOAD_AT_ONCE); return api.listTimeline(tagOrId, fromId, uptoId, LOAD_AT_ONCE);
} }
@ -1095,11 +1134,8 @@ public class TimelineFragment extends SFragment implements
} }
private void updateBottomLoadingState(FetchEnd fetchEnd) { private void updateBottomLoadingState(FetchEnd fetchEnd) {
switch (fetchEnd) { if (fetchEnd == FetchEnd.BOTTOM) {
case BOTTOM: { bottomLoading = false;
bottomLoading = false;
break;
}
} }
} }
@ -1223,8 +1259,8 @@ public class TimelineFragment extends SFragment implements
private final Function1<Status, Either<Placeholder, Status>> statusLifter = private final Function1<Status, Either<Placeholder, Status>> statusLifter =
Either.Right::new; Either.Right::new;
private @Nullable @Nullable
Pair<StatusViewData.Concrete, Integer> private Pair<StatusViewData.Concrete, Integer>
findStatusAndPosition(int position, Status status) { findStatusAndPosition(int position, Status status) {
StatusViewData.Concrete statusToUpdate; StatusViewData.Concrete statusToUpdate;
int positionToUpdate; int positionToUpdate;
@ -1260,6 +1296,13 @@ public class TimelineFragment extends SFragment implements
setFavouriteForStatus(pos, status, favEvent.getFavourite()); 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) { private void handleStatusComposeEvent(@NonNull Status status) {
switch (kind) { switch (kind) {
case HOME: case HOME:

View file

@ -45,6 +45,7 @@ import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.adapter.ThreadAdapter; import com.keylesspalace.tusky.adapter.ThreadAdapter;
import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.BlockEvent;
import com.keylesspalace.tusky.appstore.BookmarkEvent;
import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.FavoriteEvent;
import com.keylesspalace.tusky.appstore.ReblogEvent; import com.keylesspalace.tusky.appstore.ReblogEvent;
@ -186,6 +187,8 @@ public final class ViewThreadFragment extends SFragment implements
handleFavEvent((FavoriteEvent) event); handleFavEvent((FavoriteEvent) event);
} else if (event instanceof ReblogEvent) { } else if (event instanceof ReblogEvent) {
handleReblogEvent((ReblogEvent) event); handleReblogEvent((ReblogEvent) event);
} else if (event instanceof BookmarkEvent) {
handleBookmarkEvent((BookmarkEvent) event);
} else if (event instanceof BlockEvent) { } else if (event instanceof BlockEvent) {
removeAllByAccountId(((BlockEvent) event).getAccountId()); removeAllByAccountId(((BlockEvent) event).getAccountId());
} else if (event instanceof StatusComposedEvent) { } else if (event instanceof StatusComposedEvent) {
@ -239,7 +242,7 @@ public final class ViewThreadFragment extends SFragment implements
.as(autoDisposable(from(this))) .as(autoDisposable(from(this)))
.subscribe( .subscribe(
(newStatus) -> updateStatus(position, newStatus), (newStatus) -> updateStatus(position, newStatus),
(t) -> Log.d(getClass().getSimpleName(), (t) -> Log.d(TAG,
"Failed to reblog status: " + status.getId(), t) "Failed to reblog status: " + status.getId(), t)
); );
} }
@ -253,11 +256,25 @@ public final class ViewThreadFragment extends SFragment implements
.as(autoDisposable(from(this))) .as(autoDisposable(from(this)))
.subscribe( .subscribe(
(newStatus) -> updateStatus(position, newStatus), (newStatus) -> updateStatus(position, newStatus),
(t) -> Log.d(getClass().getSimpleName(), (t) -> Log.d(TAG,
"Failed to favourite status: " + status.getId(), t) "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) { private void updateStatus(int position, Status status) {
if (position >= 0 && position < statuses.size()) { if (position >= 0 && position < statuses.size()) {
@ -267,6 +284,7 @@ public final class ViewThreadFragment extends SFragment implements
.setReblogged(actionableStatus.getReblogged()) .setReblogged(actionableStatus.getReblogged())
.setReblogsCount(actionableStatus.getReblogsCount()) .setReblogsCount(actionableStatus.getReblogsCount())
.setFavourited(actionableStatus.getFavourited()) .setFavourited(actionableStatus.getFavourited())
.setBookmarked(actionableStatus.getBookmarked())
.setFavouritesCount(actionableStatus.getFavouritesCount()) .setFavouritesCount(actionableStatus.getFavouritesCount())
.createStatusViewData(); .createStatusViewData();
statuses.setPairedItem(position, viewData); statuses.setPairedItem(position, viewData);
@ -621,6 +639,28 @@ public final class ViewThreadFragment extends SFragment implements
adapter.setItem(posAndStatus.first, newViewData, true); adapter.setItem(posAndStatus.first, newViewData, true);
} }
private void handleBookmarkEvent(BookmarkEvent event) {
Pair<Integer, Status> 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) { private void handleStatusComposedEvent(StatusComposedEvent event) {
Status eventStatus = event.getStatus(); Status eventStatus = event.getStatus();
if (eventStatus.getInReplyToId() == null) return; if (eventStatus.getInReplyToId() == null) return;

View file

@ -26,6 +26,7 @@ public interface StatusActionListener extends LinkListener {
void onReply(int position); void onReply(int position);
void onReblog(final boolean reblog, final int position); void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, 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 onMore(@NonNull View view, final int position);
void onViewMedia(int position, int attachmentIndex, @Nullable View view); void onViewMedia(int position, int attachmentIndex, @Nullable View view);
void onViewThread(int position); void onViewThread(int position);

View file

@ -180,6 +180,16 @@ interface MastodonApi {
@Path("id") statusId: String @Path("id") statusId: String
): Single<Status> ): Single<Status>
@POST("api/v1/statuses/{id}/bookmark")
fun bookmarkStatus(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/unbookmark")
fun unbookmarkStatus(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/pin") @POST("api/v1/statuses/{id}/pin")
fun pinStatus( fun pinStatus(
@Path("id") statusId: String @Path("id") statusId: String
@ -343,6 +353,13 @@ interface MastodonApi {
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Call<List<Status>>
@GET("api/v1/bookmarks")
fun bookmarks(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/follow_requests") @GET("api/v1/follow_requests")
fun followRequests( fun followRequests(
@Query("max_id") maxId: String? @Query("max_id") maxId: String?

View file

@ -35,6 +35,7 @@ import java.lang.IllegalStateException
interface TimelineCases { interface TimelineCases {
fun reblog(status: Status, reblog: Boolean): Single<Status> fun reblog(status: Status, reblog: Boolean): Single<Status>
fun favourite(status: Status, favourite: Boolean): Single<Status> fun favourite(status: Status, favourite: Boolean): Single<Status>
fun bookmark(status: Status, bookmark: Boolean): Single<Status>
fun mute(id: String) fun mute(id: String)
fun block(id: String) fun block(id: String)
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
@ -80,6 +81,19 @@ class TimelineCasesImpl(
} }
} }
override fun bookmark(status: Status, bookmark: Boolean): Single<Status> {
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) { override fun mute(id: String) {
val call = mastodonApi.muteAccount(id) val call = mastodonApi.muteAccount(id)
call.enqueue(object : Callback<Relationship> { call.enqueue(object : Callback<Relationship> {

View file

@ -221,6 +221,7 @@ class TimelineRepositoryImpl(
favouritesCount = status.favouritesCount, favouritesCount = status.favouritesCount,
reblogged = status.reblogged, reblogged = status.reblogged,
favourited = status.favourited, favourited = status.favourited,
bookmarked = status.bookmarked,
sensitive = status.sensitive, sensitive = status.sensitive,
spoilerText = status.spoilerText!!, spoilerText = status.spoilerText!!,
visibility = status.visibility!!, visibility = status.visibility!!,
@ -247,6 +248,7 @@ class TimelineRepositoryImpl(
favouritesCount = 0, favouritesCount = 0,
reblogged = false, reblogged = false,
favourited = false, favourited = false,
bookmarked = false,
sensitive = false, sensitive = false,
spoilerText = "", spoilerText = "",
visibility = status.visibility!!, visibility = status.visibility!!,
@ -272,6 +274,7 @@ class TimelineRepositoryImpl(
favouritesCount = status.favouritesCount, favouritesCount = status.favouritesCount,
reblogged = status.reblogged, reblogged = status.reblogged,
favourited = status.favourited, favourited = status.favourited,
bookmarked = status.bookmarked,
sensitive = status.sensitive, sensitive = status.sensitive,
spoilerText = status.spoilerText!!, spoilerText = status.spoilerText!!,
visibility = status.visibility!!, visibility = status.visibility!!,
@ -341,6 +344,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
favouritesCount = 0, favouritesCount = 0,
reblogged = false, reblogged = false,
favourited = false, favourited = false,
bookmarked = false,
sensitive = false, sensitive = false,
spoilerText = null, spoilerText = null,
visibility = null, visibility = null,
@ -371,6 +375,7 @@ fun Status.toEntity(timelineUserId: Long,
favouritesCount = actionable.favouritesCount, favouritesCount = actionable.favouritesCount,
reblogged = actionable.reblogged, reblogged = actionable.reblogged,
favourited = actionable.favourited, favourited = actionable.favourited,
bookmarked = actionable.bookmarked,
sensitive = actionable.sensitive, sensitive = actionable.sensitive,
spoilerText = actionable.spoilerText, spoilerText = actionable.spoilerText,
visibility = actionable.visibility, visibility = actionable.visibility,

View file

@ -56,6 +56,7 @@ class ListStatusAccessibilityDelegate(
info.addAction(if (status.isReblogged) unreblogAction else reblogAction) info.addAction(if (status.isReblogged) unreblogAction else reblogAction)
} }
info.addAction(if (status.isFavourited) unfavouriteAction else favouriteAction) info.addAction(if (status.isFavourited) unfavouriteAction else favouriteAction)
info.addAction(if (status.isBookmarked) unbookmarkAction else bookmarkAction)
val mediaActions = intArrayOf( val mediaActions = intArrayOf(
R.id.action_open_media_1, R.id.action_open_media_1,
@ -95,6 +96,8 @@ class ListStatusAccessibilityDelegate(
} }
R.id.action_favourite -> statusActionListener.onFavourite(true, pos) R.id.action_favourite -> statusActionListener.onFavourite(true, pos)
R.id.action_unfavourite -> statusActionListener.onFavourite(false, 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_reblog -> statusActionListener.onReblog(true, pos)
R.id.action_unreblog -> statusActionListener.onReblog(false, pos) R.id.action_unreblog -> statusActionListener.onReblog(false, pos)
R.id.action_open_profile -> { R.id.action_open_profile -> {
@ -272,6 +275,14 @@ class ListStatusAccessibilityDelegate(
R.id.action_favourite, R.id.action_favourite,
context.getString(R.string.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( private val openProfileAction = AccessibilityActionCompat(
R.id.action_open_profile, R.id.action_open_profile,
context.getString(R.string.action_view_profile)) context.getString(R.string.action_view_profile))

View file

@ -42,6 +42,7 @@ public final class ViewDataUtils {
.setFavouritesCount(visibleStatus.getFavouritesCount()) .setFavouritesCount(visibleStatus.getFavouritesCount())
.setInReplyToId(visibleStatus.getInReplyToId()) .setInReplyToId(visibleStatus.getInReplyToId())
.setFavourited(visibleStatus.getFavourited()) .setFavourited(visibleStatus.getFavourited())
.setBookmarked(visibleStatus.getBookmarked())
.setReblogged(visibleStatus.getReblogged()) .setReblogged(visibleStatus.getReblogged())
.setIsExpanded(alwaysOpenSpoiler) .setIsExpanded(alwaysOpenSpoiler)
.setIsShowingSensitiveContent(false) .setIsShowingSensitiveContent(false)

View file

@ -42,8 +42,7 @@ import java.util.Objects;
public abstract class StatusViewData { public abstract class StatusViewData {
private StatusViewData() { private StatusViewData() { }
}
public abstract long getViewDataId(); public abstract long getViewDataId();
@ -57,6 +56,7 @@ public abstract class StatusViewData {
private final Spanned content; private final Spanned content;
final boolean reblogged; final boolean reblogged;
final boolean favourited; final boolean favourited;
final boolean bookmarked;
@Nullable @Nullable
private final String spoilerText; private final String spoilerText;
private final Status.Visibility visibility; private final Status.Visibility visibility;
@ -92,7 +92,7 @@ public abstract class StatusViewData {
private final PollViewData poll; private final PollViewData poll;
private final boolean isBot; 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<Attachment> attachments, @Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingContent, String userFullName, String nickname, String avatar, boolean isShowingContent, String userFullName, String nickname, String avatar,
@ -114,6 +114,7 @@ public abstract class StatusViewData {
} }
this.reblogged = reblogged; this.reblogged = reblogged;
this.favourited = favourited; this.favourited = favourited;
this.bookmarked = bookmarked;
this.visibility = visibility; this.visibility = visibility;
this.attachments = attachments; this.attachments = attachments;
this.rebloggedByUsername = rebloggedByUsername; this.rebloggedByUsername = rebloggedByUsername;
@ -156,6 +157,10 @@ public abstract class StatusViewData {
return favourited; return favourited;
} }
public boolean isBookmarked() {
return bookmarked;
}
@Nullable @Nullable
public String getSpoilerText() { public String getSpoilerText() {
return spoilerText; return spoilerText;
@ -288,6 +293,7 @@ public abstract class StatusViewData {
Concrete concrete = (Concrete) o; Concrete concrete = (Concrete) o;
return reblogged == concrete.reblogged && return reblogged == concrete.reblogged &&
favourited == concrete.favourited && favourited == concrete.favourited &&
bookmarked == concrete.bookmarked &&
isSensitive == concrete.isSensitive && isSensitive == concrete.isSensitive &&
isExpanded == concrete.isExpanded && isExpanded == concrete.isExpanded &&
isShowingContent == concrete.isShowingContent && isShowingContent == concrete.isShowingContent &&
@ -394,6 +400,7 @@ public abstract class StatusViewData {
private Spanned content; private Spanned content;
private boolean reblogged; private boolean reblogged;
private boolean favourited; private boolean favourited;
private boolean bookmarked;
private String spoilerText; private String spoilerText;
private Status.Visibility visibility; private Status.Visibility visibility;
private List<Attachment> attachments; private List<Attachment> attachments;
@ -429,6 +436,7 @@ public abstract class StatusViewData {
content = viewData.content; content = viewData.content;
reblogged = viewData.reblogged; reblogged = viewData.reblogged;
favourited = viewData.favourited; favourited = viewData.favourited;
bookmarked = viewData.bookmarked;
spoilerText = viewData.spoilerText; spoilerText = viewData.spoilerText;
visibility = viewData.visibility; visibility = viewData.visibility;
attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments);
@ -477,6 +485,11 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setBookmarked(boolean bookmarked) {
this.bookmarked = bookmarked;
return this;
}
public Builder setSpoilerText(String spoilerText) { public Builder setSpoilerText(String spoilerText) {
this.spoilerText = spoilerText; this.spoilerText = spoilerText;
return this; return this;
@ -626,8 +639,8 @@ public abstract class StatusViewData {
if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
if (this.createdAt == null) createdAt = new Date(); if (this.createdAt == null) createdAt = new Date();
return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility, return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText,
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot); statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot);

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?android:attr/textColorTertiary"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#19a341"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -5,7 +5,7 @@
android:id="@+id/activity_view_thread" android:id="@+id/activity_view_thread"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.FavouritesActivity"> tools:context="com.keylesspalace.tusky.StatusListActivity">
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />

View file

@ -446,7 +446,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:contentDescription="@string/action_favourite" android:contentDescription="@string/action_favourite"
android:padding="4dp" 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_constraintStart_toEndOf="@id/status_reply"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="?attr/status_favourite_active_drawable" sparkbutton:activeImage="?attr/status_favourite_active_drawable"
@ -455,6 +455,23 @@
sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" /> sparkbutton:secondaryColor="@color/tusky_orange_light" />
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark"
android:layout_width="30dp"
android:layout_height="30dp"
android:clipToPadding="false"
android:contentDescription="@string/action_bookmark"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp"
sparkbutton:primaryColor="@color/tusky_green"
sparkbutton:secondaryColor="@color/tusky_green_light" />
<ImageButton <ImageButton
android:id="@+id/status_more" android:id="@+id/status_more"
style="?attr/image_button_style" style="?attr/image_button_style"
@ -465,7 +482,7 @@
android:padding="4dp" android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply" app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_favourite" app:layout_constraintStart_toEndOf="@id/status_bookmark"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_more_horiz_24dp" /> app:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -451,7 +451,7 @@
android:contentDescription="@string/action_favourite" android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" 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_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="?attr/status_favourite_active_drawable" sparkbutton:activeImage="?attr/status_favourite_active_drawable"
@ -460,6 +460,23 @@
sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" /> sparkbutton:secondaryColor="@color/tusky_orange_light" />
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark"
android:layout_width="30dp"
android:layout_height="30dp"
android:clipToPadding="false"
android:contentDescription="@string/action_bookmark"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp"
sparkbutton:primaryColor="@color/tusky_green"
sparkbutton:secondaryColor="@color/tusky_green_light" />
<ImageButton <ImageButton
android:id="@+id/status_more" android:id="@+id/status_more"
style="?attr/image_button_style" style="?attr/image_button_style"
@ -471,7 +488,7 @@
android:padding="4dp" android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply" app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_favourite" app:layout_constraintStart_toEndOf="@id/status_bookmark"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_more_horiz_24dp" /> app:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -541,7 +541,7 @@
android:contentDescription="@string/action_favourite" android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" 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_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="?attr/status_favourite_active_drawable" sparkbutton:activeImage="?attr/status_favourite_active_drawable"
@ -550,6 +550,23 @@
sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" /> sparkbutton:secondaryColor="@color/tusky_orange_light" />
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark"
android:layout_width="40dp"
android:layout_height="40dp"
android:clipToPadding="false"
android:contentDescription="@string/action_bookmark"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp"
sparkbutton:primaryColor="@color/tusky_green"
sparkbutton:secondaryColor="@color/tusky_green_light" />
<ImageButton <ImageButton
android:id="@+id/status_more" android:id="@+id/status_more"
style="?attr/image_button_style" style="?attr/image_button_style"
@ -560,7 +577,7 @@
android:padding="4dp" android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply" app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_favourite" app:layout_constraintStart_toEndOf="@id/status_bookmark"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_more_horiz_24dp" /> app:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -4,6 +4,8 @@
<item name="action_reply" type="id" /> <item name="action_reply" type="id" />
<item name="action_favourite" type="id" /> <item name="action_favourite" type="id" />
<item name="action_unfavourite" type="id" /> <item name="action_unfavourite" type="id" />
<item name="action_bookmark" type="id" />
<item name="action_unbookmark" type="id" />
<item name="action_reblog" type="id" /> <item name="action_reblog" type="id" />
<item name="action_unreblog" type="id" /> <item name="action_unreblog" type="id" />
<item name="action_open_profile" type="id" /> <item name="action_open_profile" type="id" />

View file

@ -5,6 +5,8 @@
<color name="tusky_blue_light">#56a7e1</color> <color name="tusky_blue_light">#56a7e1</color>
<color name="tusky_orange">#ca8f04</color> <color name="tusky_orange">#ca8f04</color>
<color name="tusky_orange_light">#fab207</color> <color name="tusky_orange_light">#fab207</color>
<color name="tusky_green">#19a341</color>
<color name="tusky_green_light">#25d069</color>
<color name="toolbar_view_media">#8f000000</color> <color name="toolbar_view_media">#8f000000</color>
<color name="header_background_filter">#44000000</color> <color name="header_background_filter">#44000000</color>

View file

@ -107,8 +107,8 @@
<string name="description_status" translatable="false"> <string name="description_status" translatable="false">
<!-- Display name, cw?, content?, poll? relative date, reposted by?, reposted?, favorited?, username, media?; visibility, fav number?, reblog number?--> <!-- Display name, cw?, content?, poll? relative date, reposted by?, reposted?, favorited?, bookmarked?, username, media?; visibility, fav number?, reblog number?-->
%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
</string> </string>
<string-array name="rick_roll_domains" translatable="false"> <string-array name="rick_roll_domains" translatable="false">

View file

@ -33,6 +33,7 @@
<string name="title_follows">Follows</string> <string name="title_follows">Follows</string>
<string name="title_followers">Followers</string> <string name="title_followers">Followers</string>
<string name="title_favourites">Favorites</string> <string name="title_favourites">Favorites</string>
<string name="title_bookmarks">Bookmarks</string>
<string name="title_mutes">Muted users</string> <string name="title_mutes">Muted users</string>
<string name="title_blocks">Blocked users</string> <string name="title_blocks">Blocked users</string>
<string name="title_domain_mutes">Hidden domains</string> <string name="title_domain_mutes">Hidden domains</string>
@ -67,6 +68,7 @@
<string name="action_reblog">Boost</string> <string name="action_reblog">Boost</string>
<string name="action_unreblog">Remove boost</string> <string name="action_unreblog">Remove boost</string>
<string name="action_favourite">Favorite</string> <string name="action_favourite">Favorite</string>
<string name="action_bookmark">Bookmark</string>
<string name="action_unfavourite">Remove favorite</string> <string name="action_unfavourite">Remove favorite</string>
<string name="action_more">More</string> <string name="action_more">More</string>
<string name="action_compose">Compose</string> <string name="action_compose">Compose</string>
@ -91,6 +93,7 @@
<string name="action_view_preferences">Preferences</string> <string name="action_view_preferences">Preferences</string>
<string name="action_view_account_preferences">Account Preferences</string> <string name="action_view_account_preferences">Account Preferences</string>
<string name="action_view_favourites">Favorites</string> <string name="action_view_favourites">Favorites</string>
<string name="action_view_bookmarks">Bookmarks</string>
<string name="action_view_mutes">Muted users</string> <string name="action_view_mutes">Muted users</string>
<string name="action_view_blocks">Blocked users</string> <string name="action_view_blocks">Blocked users</string>
<string name="action_view_domain_mutes">Hidden domains</string> <string name="action_view_domain_mutes">Hidden domains</string>
@ -443,6 +446,9 @@
<string name="description_status_favourited"> <string name="description_status_favourited">
Favorited Favorited
</string> </string>
<string name="description_status_bookmarked">
Bookmarked
</string>
<string name="description_visiblity_public"> <string name="description_visiblity_public">
Public Public
</string> </string>

View file

@ -81,6 +81,7 @@ class BottomSheetActivityTest {
false, false,
false, false,
false, false,
false,
"", "",
Status.Visibility.PUBLIC, Status.Visibility.PUBLIC,
ArrayList(), ArrayList(),

View file

@ -191,6 +191,7 @@ class FilterTest {
favouritesCount = 0, favouritesCount = 0,
reblogged = false, reblogged = false,
favourited = false, favourited = false,
bookmarked = false,
sensitive = false, sensitive = false,
spoilerText = spoilerText, spoilerText = spoilerText,
visibility = Status.Visibility.PUBLIC, visibility = Status.Visibility.PUBLIC,

View file

@ -307,6 +307,7 @@ class TimelineRepositoryTest {
spoilerText = "", spoilerText = "",
reblogged = true, reblogged = true,
favourited = false, favourited = false,
bookmarked = false,
attachments = ArrayList(), attachments = ArrayList(),
mentions = arrayOf(), mentions = arrayOf(),
application = null, application = null,