* 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.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"

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" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
<activity android:name=".StatusListActivity" />
<activity android:name=".AccountListActivity" />
<activity android:name=".AboutActivity" />
<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_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<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_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);
}

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_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() {

View file

@ -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<Attachment> 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()),

View file

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

View file

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

View file

@ -69,6 +69,7 @@ data class ConversationStatusEntity(
val emojis: List<Emoji>,
val favouritesCount: Int,
val favourited: Boolean,
val bookmarked: Boolean,
val sensitive: Boolean,
val spoilerText: String,
val attachments: ArrayList<Attachment>,
@ -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,

View file

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

View file

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

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

View file

@ -187,6 +187,18 @@ class SearchViewModel @Inject constructor(
.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> {
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) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let {
more(it, view, position)

View file

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

View file

@ -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)""")

View file

@ -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?,

View file

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

View file

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

View file

@ -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<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) {
Pair<Integer, Notification> 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<Integer> choices) {
final Notification notification = notifications.get(position).asRight();
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.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<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) {
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<Status, Either<Placeholder, Status>> statusLifter =
Either.Right::new;
private @Nullable
Pair<StatusViewData.Concrete, Integer>
@Nullable
private Pair<StatusViewData.Concrete, Integer>
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:

View file

@ -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<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) {
Status eventStatus = event.getStatus();
if (eventStatus.getInReplyToId() == null) return;

View file

@ -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);

View file

@ -180,6 +180,16 @@ interface MastodonApi {
@Path("id") statusId: String
): 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")
fun pinStatus(
@Path("id") statusId: String
@ -343,6 +353,13 @@ interface MastodonApi {
@Query("limit") limit: Int?
): 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")
fun followRequests(
@Query("max_id") maxId: String?

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Attachment> 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<Attachment> 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);

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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.FavouritesActivity">
tools:context="com.keylesspalace.tusky.StatusListActivity">
<include layout="@layout/toolbar_basic" />

View file

@ -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" />
<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
android:id="@+id/status_more"
style="?attr/image_button_style"
@ -465,7 +482,7 @@
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
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:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -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" />
<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
android:id="@+id/status_more"
style="?attr/image_button_style"
@ -471,7 +488,7 @@
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
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:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -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" />
<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
android:id="@+id/status_more"
style="?attr/image_button_style"
@ -560,7 +577,7 @@
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
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:srcCompat="@drawable/ic_more_horiz_24dp" />

View file

@ -4,6 +4,8 @@
<item name="action_reply" type="id" />
<item name="action_favourite" 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_unreblog" 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_orange">#ca8f04</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="header_background_filter">#44000000</color>

View file

@ -107,8 +107,8 @@
<string name="description_status" translatable="false">
<!-- Display name, cw?, content?, poll? relative date, reposted by?, reposted?, favorited?, 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
<!-- 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, %14$s %4$s, %5$s; %6$s, %7$s, %8$s, %9$s, %10$s; %11$s, %12$s, %13$s
</string>
<string-array name="rick_roll_domains" translatable="false">

View file

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

View file

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

View file

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

View file

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